@symbiote-native/angular 0.0.0-canary-20260724081123
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/LICENSE +21 -0
- package/README.md +157 -0
- package/babel-linker.cjs +7 -0
- package/babel-register-composed.cjs +127 -0
- package/bin/symbiote-angular-dev.cjs +93 -0
- package/build/angular/anchor-host-registry.d.ts +2 -0
- package/build/angular/anchor-host-registry.js +84 -0
- package/build/angular/anchor-host-registry.js.map +1 -0
- package/build/angular/bootstrap.d.ts +7 -0
- package/build/angular/bootstrap.js +15 -0
- package/build/angular/bootstrap.js.map +1 -0
- package/build/angular/components/activity-indicator/index.android.d.ts +12 -0
- package/build/angular/components/activity-indicator/index.android.js +21 -0
- package/build/angular/components/activity-indicator/index.android.js.map +1 -0
- package/build/angular/components/activity-indicator/index.d.ts +1 -0
- package/build/angular/components/activity-indicator/index.ios.d.ts +8 -0
- package/build/angular/components/activity-indicator/index.ios.js +21 -0
- package/build/angular/components/activity-indicator/index.ios.js.map +1 -0
- package/build/angular/components/activity-indicator/index.js +4 -0
- package/build/angular/components/activity-indicator/index.js.map +1 -0
- package/build/angular/components/activity-indicator/shared.d.ts +67 -0
- package/build/angular/components/activity-indicator/shared.js +260 -0
- package/build/angular/components/activity-indicator/shared.js.map +1 -0
- package/build/angular/components/button.d.ts +63 -0
- package/build/angular/components/button.js +332 -0
- package/build/angular/components/button.js.map +1 -0
- package/build/angular/components/flat-list/index.d.ts +122 -0
- package/build/angular/components/flat-list/index.js +688 -0
- package/build/angular/components/flat-list/index.js.map +1 -0
- package/build/angular/components/image/index.android.d.ts +10 -0
- package/build/angular/components/image/index.android.js +59 -0
- package/build/angular/components/image/index.android.js.map +1 -0
- package/build/angular/components/image/index.d.ts +1 -0
- package/build/angular/components/image/index.ios.d.ts +10 -0
- package/build/angular/components/image/index.ios.js +59 -0
- package/build/angular/components/image/index.ios.js.map +1 -0
- package/build/angular/components/image/index.js +3 -0
- package/build/angular/components/image/index.js.map +1 -0
- package/build/angular/components/image/shared.d.ts +101 -0
- package/build/angular/components/image/shared.js +406 -0
- package/build/angular/components/image/shared.js.map +1 -0
- package/build/angular/components/image-background.d.ts +81 -0
- package/build/angular/components/image-background.js +418 -0
- package/build/angular/components/image-background.js.map +1 -0
- package/build/angular/components/input-accessory-view/index.d.ts +60 -0
- package/build/angular/components/input-accessory-view/index.js +251 -0
- package/build/angular/components/input-accessory-view/index.js.map +1 -0
- package/build/angular/components/keyboard-avoiding-view/index.d.ts +82 -0
- package/build/angular/components/keyboard-avoiding-view/index.js +358 -0
- package/build/angular/components/keyboard-avoiding-view/index.js.map +1 -0
- package/build/angular/components/modal/index.d.ts +92 -0
- package/build/angular/components/modal/index.js +360 -0
- package/build/angular/components/modal/index.js.map +1 -0
- package/build/angular/components/pressable/index.d.ts +123 -0
- package/build/angular/components/pressable/index.js +420 -0
- package/build/angular/components/pressable/index.js.map +1 -0
- package/build/angular/components/refresh-control/index.d.ts +86 -0
- package/build/angular/components/refresh-control/index.js +342 -0
- package/build/angular/components/refresh-control/index.js.map +1 -0
- package/build/angular/components/safe-area-view/index.d.ts +57 -0
- package/build/angular/components/safe-area-view/index.js +248 -0
- package/build/angular/components/safe-area-view/index.js.map +1 -0
- package/build/angular/components/scroll-view/index.android.d.ts +21 -0
- package/build/angular/components/scroll-view/index.android.js +235 -0
- package/build/angular/components/scroll-view/index.android.js.map +1 -0
- package/build/angular/components/scroll-view/index.d.ts +3 -0
- package/build/angular/components/scroll-view/index.ios.d.ts +9 -0
- package/build/angular/components/scroll-view/index.ios.js +86 -0
- package/build/angular/components/scroll-view/index.ios.js.map +1 -0
- package/build/angular/components/scroll-view/index.js +6 -0
- package/build/angular/components/scroll-view/index.js.map +1 -0
- package/build/angular/components/scroll-view/projection.d.ts +32 -0
- package/build/angular/components/scroll-view/projection.js +334 -0
- package/build/angular/components/scroll-view/projection.js.map +1 -0
- package/build/angular/components/scroll-view/shared.d.ts +230 -0
- package/build/angular/components/scroll-view/shared.js +682 -0
- package/build/angular/components/scroll-view/shared.js.map +1 -0
- package/build/angular/components/scroll-view/sticky-header.d.ts +40 -0
- package/build/angular/components/scroll-view/sticky-header.js +225 -0
- package/build/angular/components/scroll-view/sticky-header.js.map +1 -0
- package/build/angular/components/section-list/index.d.ts +116 -0
- package/build/angular/components/section-list/index.js +641 -0
- package/build/angular/components/section-list/index.js.map +1 -0
- package/build/angular/components/switch/index.android.d.ts +20 -0
- package/build/angular/components/switch/index.android.js +70 -0
- package/build/angular/components/switch/index.android.js.map +1 -0
- package/build/angular/components/switch/index.d.ts +1 -0
- package/build/angular/components/switch/index.ios.d.ts +19 -0
- package/build/angular/components/switch/index.ios.js +69 -0
- package/build/angular/components/switch/index.ios.js.map +1 -0
- package/build/angular/components/switch/index.js +4 -0
- package/build/angular/components/switch/index.js.map +1 -0
- package/build/angular/components/switch/shared.d.ts +40 -0
- package/build/angular/components/switch/shared.js +127 -0
- package/build/angular/components/switch/shared.js.map +1 -0
- package/build/angular/components/text-input.d.ts +153 -0
- package/build/angular/components/text-input.js +599 -0
- package/build/angular/components/text-input.js.map +1 -0
- package/build/angular/components/touchable/index.d.ts +245 -0
- package/build/angular/components/touchable/index.js +1186 -0
- package/build/angular/components/touchable/index.js.map +1 -0
- package/build/angular/components/touchable-native-feedback/index.d.ts +83 -0
- package/build/angular/components/touchable-native-feedback/index.js +371 -0
- package/build/angular/components/touchable-native-feedback/index.js.map +1 -0
- package/build/angular/components/virtualized-list/directives.d.ts +54 -0
- package/build/angular/components/virtualized-list/directives.js +153 -0
- package/build/angular/components/virtualized-list/directives.js.map +1 -0
- package/build/angular/components/virtualized-list/index.d.ts +253 -0
- package/build/angular/components/virtualized-list/index.js +1031 -0
- package/build/angular/components/virtualized-list/index.js.map +1 -0
- package/build/angular/components/virtualized-section-list/directives.d.ts +37 -0
- package/build/angular/components/virtualized-section-list/directives.js +76 -0
- package/build/angular/components/virtualized-section-list/directives.js.map +1 -0
- package/build/angular/components/virtualized-section-list/index.d.ts +178 -0
- package/build/angular/components/virtualized-section-list/index.js +720 -0
- package/build/angular/components/virtualized-section-list/index.js.map +1 -0
- package/build/angular/components.d.ts +39 -0
- package/build/angular/components.js +38 -0
- package/build/angular/components.js.map +1 -0
- package/build/angular/create-portal/index.d.ts +22 -0
- package/build/angular/create-portal/index.js +74 -0
- package/build/angular/create-portal/index.js.map +1 -0
- package/build/angular/create-tunnel/index.d.ts +34 -0
- package/build/angular/create-tunnel/index.js +126 -0
- package/build/angular/create-tunnel/index.js.map +1 -0
- package/build/angular/descriptor-to-angular/index.d.ts +22 -0
- package/build/angular/descriptor-to-angular/index.js +142 -0
- package/build/angular/descriptor-to-angular/index.js.map +1 -0
- package/build/angular/host-instance/index.d.ts +2 -0
- package/build/angular/host-instance/index.js +37 -0
- package/build/angular/host-instance/index.js.map +1 -0
- package/build/angular/index.d.ts +22 -0
- package/build/angular/index.js +46 -0
- package/build/angular/index.js.map +1 -0
- package/build/angular/modules/animated/animated-leaf-binder/index.d.ts +14 -0
- package/build/angular/modules/animated/animated-leaf-binder/index.js +91 -0
- package/build/angular/modules/animated/animated-leaf-binder/index.js.map +1 -0
- package/build/angular/modules/animated/animated-leaf-binder.d.ts +14 -0
- package/build/angular/modules/animated/animated-leaf-binder.js +91 -0
- package/build/angular/modules/animated/animated-leaf-binder.js.map +1 -0
- package/build/angular/modules/animated/create-animated-component.d.ts +59 -0
- package/build/angular/modules/animated/create-animated-component.js +364 -0
- package/build/angular/modules/animated/create-animated-component.js.map +1 -0
- package/build/angular/modules/animated/index.d.ts +51 -0
- package/build/angular/modules/animated/index.js +59 -0
- package/build/angular/modules/animated/index.js.map +1 -0
- package/build/angular/modules/app-registry/index.d.ts +7 -0
- package/build/angular/modules/app-registry/index.js +23 -0
- package/build/angular/modules/app-registry/index.js.map +1 -0
- package/build/angular/modules/status-bar/index.d.ts +20 -0
- package/build/angular/modules/status-bar/index.js +59 -0
- package/build/angular/modules/status-bar/index.js.map +1 -0
- package/build/angular/primitives/index.d.ts +63 -0
- package/build/angular/primitives/index.js +189 -0
- package/build/angular/primitives/index.js.map +1 -0
- package/build/angular/primitives/shared.d.ts +88 -0
- package/build/angular/primitives/shared.js +171 -0
- package/build/angular/primitives/shared.js.map +1 -0
- package/build/angular/render/index.d.ts +11 -0
- package/build/angular/render/index.js +171 -0
- package/build/angular/render/index.js.map +1 -0
- package/build/angular/renderer/index.d.ts +37 -0
- package/build/angular/renderer/index.js +301 -0
- package/build/angular/renderer/index.js.map +1 -0
- package/build/angular/services/color-scheme.service.d.ts +16 -0
- package/build/angular/services/color-scheme.service.js +31 -0
- package/build/angular/services/color-scheme.service.js.map +1 -0
- package/build/angular/services/index.d.ts +2 -0
- package/build/angular/services/index.js +3 -0
- package/build/angular/services/index.js.map +1 -0
- package/build/angular/services/window-dimensions.service.d.ts +16 -0
- package/build/angular/services/window-dimensions.service.js +42 -0
- package/build/angular/services/window-dimensions.service.js.map +1 -0
- package/metro-config.cjs +48 -0
- package/metro-css-parser.cjs +13 -0
- package/package.json +80 -0
- package/src/__tests__/responder-change-detection.test.ts +102 -0
- package/src/__tests__/responder-nested-cd.test.ts +113 -0
- package/src/anchor-host-registry.ts +88 -0
- package/src/angular-gaps.test.ts +142 -0
- package/src/angular-imports.test.ts +100 -0
- package/src/bootstrap.ts +26 -0
- package/src/components/activity-indicator/activity-indicator-class-style.test.ts +44 -0
- package/src/components/activity-indicator/activity-indicator.test.ts +131 -0
- package/src/components/activity-indicator/index.android.ts +16 -0
- package/src/components/activity-indicator/index.ios.ts +17 -0
- package/src/components/activity-indicator/index.ts +4 -0
- package/src/components/activity-indicator/shared.ts +206 -0
- package/src/components/button.ts +193 -0
- package/src/components/flat-list/column-wrapper-style-class.test.ts +116 -0
- package/src/components/flat-list/flat-list-array-style.test.ts +131 -0
- package/src/components/flat-list/flat-list-class-style.test.ts +74 -0
- package/src/components/flat-list/flat-list-scroll-containment.test.ts +107 -0
- package/src/components/flat-list/flat-list.test.ts +99 -0
- package/src/components/flat-list/index.ts +574 -0
- package/src/components/image/image-class-style.test.ts +46 -0
- package/src/components/image/index.android.ts +48 -0
- package/src/components/image/index.ios.ts +48 -0
- package/src/components/image/index.ts +3 -0
- package/src/components/image/shared.ts +479 -0
- package/src/components/image-background-class-style.test.ts +48 -0
- package/src/components/image-background-style-class.test.ts +79 -0
- package/src/components/image-background.ts +270 -0
- package/src/components/input-accessory-view/index.ts +185 -0
- package/src/components/input-accessory-view/input-accessory-view.test.ts +43 -0
- package/src/components/keyboard-avoiding-view/index.ts +303 -0
- package/src/components/keyboard-avoiding-view/keyboard-avoiding-view.test.ts +54 -0
- package/src/components/modal/index.ts +299 -0
- package/src/components/modal/modal.test.ts +43 -0
- package/src/components/pressable/index.ts +373 -0
- package/src/components/pressable/pressable.test.ts +85 -0
- package/src/components/refresh-control/index.ts +284 -0
- package/src/components/refresh-control/refresh-control.test.ts +41 -0
- package/src/components/safe-area-view/index.ts +177 -0
- package/src/components/safe-area-view/safe-area-view.test.ts +43 -0
- package/src/components/scroll-view/android-scroll-view-axis-projection.test.ts +123 -0
- package/src/components/scroll-view/content-container-style-class.test.ts +75 -0
- package/src/components/scroll-view/index.android.ts +190 -0
- package/src/components/scroll-view/index.ios.ts +79 -0
- package/src/components/scroll-view/index.ts +7 -0
- package/src/components/scroll-view/projection.ts +414 -0
- package/src/components/scroll-view/scroll-view-class-style.test.ts +134 -0
- package/src/components/scroll-view/scroll-view-projection.test.ts +194 -0
- package/src/components/scroll-view/shared.ts +844 -0
- package/src/components/scroll-view/sticky-header.ts +255 -0
- package/src/components/section-list/index.ts +445 -0
- package/src/components/switch/android-switch-class.test.ts +42 -0
- package/src/components/switch/index.android.ts +61 -0
- package/src/components/switch/index.ios.ts +60 -0
- package/src/components/switch/index.ts +4 -0
- package/src/components/switch/shared.ts +197 -0
- package/src/components/switch/switch.test.ts +92 -0
- package/src/components/text-input-class-style.test.ts +54 -0
- package/src/components/text-input.ts +531 -0
- package/src/components/touchable/index.ts +629 -0
- package/src/components/touchable/touchable.test.ts +102 -0
- package/src/components/touchable-native-feedback/index.ts +254 -0
- package/src/components/touchable-native-feedback/touchable-native-feedback.test.ts +49 -0
- package/src/components/virtualized-list/directives.ts +168 -0
- package/src/components/virtualized-list/index.ts +992 -0
- package/src/components/virtualized-list/virtualized-list-class-style.test.ts +76 -0
- package/src/components/virtualized-section-list/directives.ts +90 -0
- package/src/components/virtualized-section-list/index.ts +601 -0
- package/src/components/virtualized-section-list/virtualized-section-list-class-style.test.ts +60 -0
- package/src/components.ts +149 -0
- package/src/create-portal/create-portal.test.ts +90 -0
- package/src/create-portal/index.ts +74 -0
- package/src/create-tunnel/create-tunnel.test.ts +120 -0
- package/src/create-tunnel/index.ts +136 -0
- package/src/descriptor-to-angular/descriptor-outlet.test.ts +94 -0
- package/src/descriptor-to-angular/index.ts +190 -0
- package/src/host-instance/host-instance.test.ts +49 -0
- package/src/host-instance/index.ts +36 -0
- package/src/index.ts +261 -0
- package/src/modules/animated/animated-leaf-binder/animated-leaf-binder.test.ts +116 -0
- package/src/modules/animated/animated-leaf-binder/index.ts +101 -0
- package/src/modules/animated/animated-leaf-binder.test.ts +116 -0
- package/src/modules/animated/animated-leaf-binder.ts +101 -0
- package/src/modules/animated/animated-scroll-view.test.ts +102 -0
- package/src/modules/animated/create-animated-component.ts +365 -0
- package/src/modules/animated/index.ts +105 -0
- package/src/modules/app-registry/app-registry.test.ts +106 -0
- package/src/modules/app-registry/index.ts +60 -0
- package/src/modules/status-bar/index.ts +57 -0
- package/src/modules/status-bar/status-bar.test.ts +50 -0
- package/src/primitives/index.ts +119 -0
- package/src/primitives/shared.ts +190 -0
- package/src/render/index.ts +235 -0
- package/src/render/render.test.ts +147 -0
- package/src/renderer/index.ts +347 -0
- package/src/renderer/renderer.test.ts +203 -0
- package/src/services/color-scheme.service.ts +27 -0
- package/src/services/index.ts +2 -0
- package/src/services/services.test.ts +163 -0
- package/src/services/window-dimensions.service.ts +41 -0
- package/tsconfig.angular.base.json +30 -0
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
// KeyboardAvoidingView: the Angular lifecycle half. A plain View that shifts out of the
|
|
2
|
+
// keyboard's way as it shows/hides. The inset math + the behavior → style/structure decision
|
|
3
|
+
// live framework-agnostic in @symbiote-native/components (render-keyboard-avoiding-view), shared verbatim
|
|
4
|
+
// with React/Vue; Angular supplies only the lifecycle: a plain inset field, ngOnInit subscribes to
|
|
5
|
+
// the core Keyboard module (show / changeFrame / hide) and markForCheck pulls the OnPush view (the
|
|
6
|
+
// Angular twin of React's setState / Vue's reactive ref), ngOnDestroy tears the subscriptions down,
|
|
7
|
+
// and the wrapper's onLayout measures the frame that feeds the next event's inset. The user
|
|
8
|
+
// children nest under the wrapper (or, for 'position', an inner View) via <ng-content>. No native
|
|
9
|
+
// host of its own — it wraps symbiote-view — so this stays a flat single file.
|
|
10
|
+
//
|
|
11
|
+
// Full parity: behavior 'height'|'position'|'padding', enabled, keyboardVerticalOffset,
|
|
12
|
+
// contentContainerStyle, onLayout, plus the full a11y/aria surface every View carries.
|
|
13
|
+
|
|
14
|
+
import {
|
|
15
|
+
CUSTOM_ELEMENTS_SCHEMA,
|
|
16
|
+
ChangeDetectionStrategy,
|
|
17
|
+
ChangeDetectorRef,
|
|
18
|
+
Component,
|
|
19
|
+
ElementRef,
|
|
20
|
+
EventEmitter,
|
|
21
|
+
inject,
|
|
22
|
+
Input,
|
|
23
|
+
Output,
|
|
24
|
+
type OnDestroy,
|
|
25
|
+
type OnInit,
|
|
26
|
+
} from '@angular/core';
|
|
27
|
+
import {
|
|
28
|
+
computeInset,
|
|
29
|
+
readKeyboardFrame,
|
|
30
|
+
readLayoutFrame,
|
|
31
|
+
resolveAccessibilityProps,
|
|
32
|
+
resolveKeyboardAvoidingLayout,
|
|
33
|
+
DEFAULT_VERTICAL_OFFSET,
|
|
34
|
+
type IAccessibilityProps,
|
|
35
|
+
type IAccessibilityStateValue,
|
|
36
|
+
type IAriaProps,
|
|
37
|
+
type IKeyboardAvoidingBehavior,
|
|
38
|
+
type IKeyboardAvoidingLayout,
|
|
39
|
+
type IMeasuredFrame,
|
|
40
|
+
} from '@symbiote-native/components';
|
|
41
|
+
import {
|
|
42
|
+
Keyboard,
|
|
43
|
+
KEYBOARD_EVENT,
|
|
44
|
+
dlog,
|
|
45
|
+
isSymbioteEvent,
|
|
46
|
+
type IEventSubscription,
|
|
47
|
+
type IStyleProp,
|
|
48
|
+
type ISymbioteEvent,
|
|
49
|
+
type IViewStyle,
|
|
50
|
+
} from '@symbiote-native/engine';
|
|
51
|
+
import { anchorHostStyle, SymbioteHostPropsDirective, ViewHost } from '../../primitives';
|
|
52
|
+
|
|
53
|
+
export type { IKeyboardAvoidingBehavior } from '@symbiote-native/components';
|
|
54
|
+
|
|
55
|
+
// Mirrors React's IKeyboardAvoidingViewProps minus children (Angular takes children via
|
|
56
|
+
// <ng-content>), declared per-adapter over the shared accessibility base since a framework-specific
|
|
57
|
+
// children field keeps it from being fully shared across adapters.
|
|
58
|
+
export interface IAngularKeyboardAvoidingViewProps extends IAccessibilityProps, IAriaProps {
|
|
59
|
+
behavior?: IKeyboardAvoidingBehavior;
|
|
60
|
+
enabled?: boolean;
|
|
61
|
+
keyboardVerticalOffset?: number;
|
|
62
|
+
contentContainerStyle?: IStyleProp<IViewStyle>;
|
|
63
|
+
style?: IStyleProp<IViewStyle>;
|
|
64
|
+
onLayout?: (event: ISymbioteEvent) => void;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// What the component itself takes as plain @Input()s: the full surface minus onLayout and the
|
|
68
|
+
// accessibility callbacks, which it exposes as real @Output() EventEmitters instead (see the
|
|
69
|
+
// class below), mirroring Pressable's IAngularPressableInputs split.
|
|
70
|
+
export type IAngularKeyboardAvoidingViewInputs = Omit<
|
|
71
|
+
IAngularKeyboardAvoidingViewProps,
|
|
72
|
+
| 'onLayout'
|
|
73
|
+
| 'onAccessibilityAction'
|
|
74
|
+
| 'onAccessibilityTap'
|
|
75
|
+
| 'onMagicTap'
|
|
76
|
+
| 'onAccessibilityEscape'
|
|
77
|
+
>;
|
|
78
|
+
|
|
79
|
+
@Component({
|
|
80
|
+
selector: 'KeyboardAvoidingView',
|
|
81
|
+
standalone: true,
|
|
82
|
+
schemas: [CUSTOM_ELEMENTS_SCHEMA],
|
|
83
|
+
imports: [ViewHost, SymbioteHostPropsDirective],
|
|
84
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
85
|
+
// The wrapper carries a11y + onLayout; 'position' ('nested') pushes the children in an inner
|
|
86
|
+
// View by `bottom: inset`, the wrapper modes adjust the single wrapper directly. Only one @if
|
|
87
|
+
// branch is instantiated, so each branch's <ng-content> projects the children unambiguously.
|
|
88
|
+
template: `
|
|
89
|
+
<symbiote-view
|
|
90
|
+
[symbioteHostProps]="hostProps"
|
|
91
|
+
(accessibilityAction)="emit(accessibilityAction, $event)"
|
|
92
|
+
(accessibilityTap)="emit(accessibilityTap, $event)"
|
|
93
|
+
(magicTap)="emit(magicTap, $event)"
|
|
94
|
+
(accessibilityEscape)="emit(accessibilityEscape, $event)"
|
|
95
|
+
(layout)="handleLayout($event)"
|
|
96
|
+
>
|
|
97
|
+
@if (isNested) {
|
|
98
|
+
<symbiote-view [style]="innerStyle">
|
|
99
|
+
<ng-content></ng-content>
|
|
100
|
+
</symbiote-view>
|
|
101
|
+
} @else {
|
|
102
|
+
<ng-content></ng-content>
|
|
103
|
+
}
|
|
104
|
+
</symbiote-view>
|
|
105
|
+
`,
|
|
106
|
+
})
|
|
107
|
+
export class KeyboardAvoidingView implements IAngularKeyboardAvoidingViewInputs, OnInit, OnDestroy {
|
|
108
|
+
@Input() behavior?: IKeyboardAvoidingBehavior;
|
|
109
|
+
@Input() enabled?: boolean;
|
|
110
|
+
@Input() keyboardVerticalOffset?: number;
|
|
111
|
+
@Input() contentContainerStyle?: IStyleProp<IViewStyle>;
|
|
112
|
+
@Input() style?: IStyleProp<IViewStyle>;
|
|
113
|
+
// The wrapper's onLayout is a real @Output(): `(layout)="…"`, not `[onLayout]="…"`. Safe to
|
|
114
|
+
// name it the same as the native `layout` event fired inside this component's own template
|
|
115
|
+
// (see the class's `handleLayout`) — the engine's bubble() treats ANCHOR_HOST_COMPONENTS as
|
|
116
|
+
// transparent to listener lookup, so there is no double-fire.
|
|
117
|
+
@Output() readonly layout = new EventEmitter<ISymbioteEvent>();
|
|
118
|
+
@Output() readonly accessibilityAction = new EventEmitter<ISymbioteEvent>();
|
|
119
|
+
@Output() readonly accessibilityTap = new EventEmitter<ISymbioteEvent>();
|
|
120
|
+
@Output() readonly magicTap = new EventEmitter<ISymbioteEvent>();
|
|
121
|
+
@Output() readonly accessibilityEscape = new EventEmitter<ISymbioteEvent>();
|
|
122
|
+
@Input() testID?: string;
|
|
123
|
+
@Input() nativeID?: string;
|
|
124
|
+
@Input() accessible?: boolean;
|
|
125
|
+
@Input() accessibilityLabel?: string;
|
|
126
|
+
@Input() accessibilityHint?: string;
|
|
127
|
+
@Input() accessibilityRole?: IAccessibilityProps['accessibilityRole'];
|
|
128
|
+
@Input() accessibilityState?: IAccessibilityStateValue;
|
|
129
|
+
@Input() accessibilityValue?: IAccessibilityProps['accessibilityValue'];
|
|
130
|
+
@Input() accessibilityActions?: IAccessibilityProps['accessibilityActions'];
|
|
131
|
+
@Input() accessibilityLabelledBy?: string | string[];
|
|
132
|
+
@Input() importantForAccessibility?: IAccessibilityProps['importantForAccessibility'];
|
|
133
|
+
@Input() accessibilityLiveRegion?: IAccessibilityProps['accessibilityLiveRegion'];
|
|
134
|
+
@Input() screenReaderFocusable?: boolean;
|
|
135
|
+
@Input() accessibilityViewIsModal?: boolean;
|
|
136
|
+
@Input() accessibilityElementsHidden?: boolean;
|
|
137
|
+
@Input() accessibilityIgnoresInvertColors?: boolean;
|
|
138
|
+
@Input() accessibilityLanguage?: string;
|
|
139
|
+
@Input() accessibilityRespondsToUserInteraction?: boolean;
|
|
140
|
+
@Input() accessibilityShowsLargeContentViewer?: boolean;
|
|
141
|
+
@Input() accessibilityLargeContentTitle?: string;
|
|
142
|
+
@Input() role?: IAriaProps['role'];
|
|
143
|
+
@Input('aria-label') ariaLabel?: string;
|
|
144
|
+
@Input('aria-labelledby') ariaLabelledBy?: string;
|
|
145
|
+
@Input('aria-live') ariaLive?: IAriaProps['aria-live'];
|
|
146
|
+
@Input('aria-hidden') ariaHidden?: boolean;
|
|
147
|
+
@Input('aria-busy') ariaBusy?: boolean;
|
|
148
|
+
@Input('aria-checked') ariaChecked?: boolean | 'mixed';
|
|
149
|
+
@Input('aria-disabled') ariaDisabled?: boolean;
|
|
150
|
+
@Input('aria-expanded') ariaExpanded?: boolean;
|
|
151
|
+
@Input('aria-selected') ariaSelected?: boolean;
|
|
152
|
+
@Input('aria-modal') ariaModal?: boolean;
|
|
153
|
+
@Input('aria-valuemax') ariaValueMax?: number;
|
|
154
|
+
@Input('aria-valuemin') ariaValueMin?: number;
|
|
155
|
+
@Input('aria-valuenow') ariaValueNow?: number;
|
|
156
|
+
@Input('aria-valuetext') ariaValueText?: string;
|
|
157
|
+
|
|
158
|
+
// How far the view must move so it no longer overlaps the keyboard. A plain field, not reactive:
|
|
159
|
+
// OnPush + zoneless means a keyboard event mutates it and pulls the view via markForCheck.
|
|
160
|
+
private inset = 0;
|
|
161
|
+
// Mutable, not state: changing the measured frame alone shouldn't re-render; it feeds the next
|
|
162
|
+
// keyboard event's inset math (React's frameRef / initialHeightRef, Vue's frame / initialHeight).
|
|
163
|
+
private frame?: IMeasuredFrame;
|
|
164
|
+
private initialHeight?: number;
|
|
165
|
+
private subscriptions: IEventSubscription[] = [];
|
|
166
|
+
|
|
167
|
+
private readonly changeDetector = inject(ChangeDetectorRef);
|
|
168
|
+
// This component's OWN host — the non-painting anchor `class="..."` at the use site resolves
|
|
169
|
+
// onto (see anchorHostStyle's doc comment) — NOT a ViewChild into the inner symbiote-view (this
|
|
170
|
+
// component has none; the wrapper IS the outer template node hostProps binds onto).
|
|
171
|
+
private readonly elementRef = inject(ElementRef);
|
|
172
|
+
|
|
173
|
+
ngOnInit(): void {
|
|
174
|
+
this.subscriptions = [
|
|
175
|
+
Keyboard.addListener(KEYBOARD_EVENT.didShow, payload => this.onShow(payload)),
|
|
176
|
+
Keyboard.addListener(KEYBOARD_EVENT.didChangeFrame, payload => this.onShow(payload)),
|
|
177
|
+
Keyboard.addListener(KEYBOARD_EVENT.didHide, () => this.onHide()),
|
|
178
|
+
];
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
ngOnDestroy(): void {
|
|
182
|
+
for (const subscription of this.subscriptions) subscription.remove();
|
|
183
|
+
this.subscriptions = [];
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
private onShow(payload: unknown): void {
|
|
187
|
+
const keyboard = readKeyboardFrame(payload);
|
|
188
|
+
const offset = this.keyboardVerticalOffset ?? DEFAULT_VERTICAL_OFFSET;
|
|
189
|
+
const next = computeInset(this.frame, keyboard, offset);
|
|
190
|
+
dlog(`KeyboardAvoidingView show -> inset ${next}`);
|
|
191
|
+
this.inset = next;
|
|
192
|
+
this.changeDetector.markForCheck();
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
private onHide(): void {
|
|
196
|
+
dlog('KeyboardAvoidingView hide -> inset 0');
|
|
197
|
+
this.inset = 0;
|
|
198
|
+
this.changeDetector.markForCheck();
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// The wrapper's onLayout measures the frame BEFORE forwarding to the caller's onLayout, so the
|
|
202
|
+
// next keyboard event's inset math has the view's real position (React/Vue's handleLayout).
|
|
203
|
+
handleLayout(event: unknown): void {
|
|
204
|
+
if (!isSymbioteEvent(event)) return;
|
|
205
|
+
const measured = readLayoutFrame(event.nativeEvent.layout);
|
|
206
|
+
if (measured !== undefined) {
|
|
207
|
+
this.frame = measured;
|
|
208
|
+
if (this.initialHeight === undefined) this.initialHeight = measured.height;
|
|
209
|
+
}
|
|
210
|
+
this.layout.emit(event);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// RN gates every inset on `enabled ?? true`; only an explicit `false` disables, forcing the inset
|
|
214
|
+
// to 0 so every behavior mode renders the view untouched.
|
|
215
|
+
private get effectiveInset(): number {
|
|
216
|
+
return this.enabled === false ? 0 : this.inset;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// The behavior + effective inset → wrapper/inner styles and the nesting decision (the shared core
|
|
220
|
+
// of RN's render). Recomputed per CD pass, the Angular twin of React/Vue's per-render call.
|
|
221
|
+
// Named `resolvedLayout`, not `layout` — that name is now the onLayout @Output() EventEmitter.
|
|
222
|
+
private get resolvedLayout(): IKeyboardAvoidingLayout {
|
|
223
|
+
return resolveKeyboardAvoidingLayout({
|
|
224
|
+
behavior: this.behavior,
|
|
225
|
+
effectiveInset: this.effectiveInset,
|
|
226
|
+
initialHeight: this.initialHeight,
|
|
227
|
+
style: this.style,
|
|
228
|
+
contentContainerStyle: this.contentContainerStyle,
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
get isNested(): boolean {
|
|
233
|
+
return this.resolvedLayout.kind === 'nested';
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
get wrapperStyle(): IStyleProp<IViewStyle> | undefined {
|
|
237
|
+
return this.resolvedLayout.wrapperStyle;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
get innerStyle(): IStyleProp<IViewStyle> | undefined {
|
|
241
|
+
return this.resolvedLayout.kind === 'nested' ? this.resolvedLayout.innerStyle : undefined;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// The wrapper's full prop bag (style + identity + a11y) folded for `[symbioteHostProps]`,
|
|
245
|
+
// the same pattern Modal's `hostProps` uses over its renderModal descriptor. The anchor's
|
|
246
|
+
// class-derived style goes FIRST, the resolved wrapper style SECOND — flattenStyle's later-wins
|
|
247
|
+
// collapse keeps an explicit [style] winning over its ambient class.
|
|
248
|
+
get hostProps(): Record<string, unknown> {
|
|
249
|
+
return {
|
|
250
|
+
style: [anchorHostStyle(this.elementRef), this.wrapperStyle],
|
|
251
|
+
testID: this.testID,
|
|
252
|
+
nativeID: this.nativeID,
|
|
253
|
+
accessible: this.accessible,
|
|
254
|
+
...this.folded,
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// Forward an engine event to the matching @Output(), narrowing the template's untyped $event
|
|
259
|
+
// first. The accessibility* Outputs ride the engine's structural event channel (Angular blocks
|
|
260
|
+
// [onX] property bindings; events flow through (event) only).
|
|
261
|
+
emit(emitter: EventEmitter<ISymbioteEvent>, event: unknown): void {
|
|
262
|
+
if (isSymbioteEvent(event)) emitter.emit(event);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// Fold the web aria-*/role aliases into the canonical accessibility* props once per render, so the
|
|
266
|
+
// host node never sees an aria-* key (native ignores them) — the shared transform every adapter runs.
|
|
267
|
+
get folded(): Partial<IAngularKeyboardAvoidingViewProps> {
|
|
268
|
+
return resolveAccessibilityProps({
|
|
269
|
+
accessibilityLabel: this.accessibilityLabel,
|
|
270
|
+
accessibilityHint: this.accessibilityHint,
|
|
271
|
+
accessibilityRole: this.accessibilityRole,
|
|
272
|
+
accessibilityState: this.accessibilityState,
|
|
273
|
+
accessibilityValue: this.accessibilityValue,
|
|
274
|
+
accessibilityActions: this.accessibilityActions,
|
|
275
|
+
accessibilityLabelledBy: this.accessibilityLabelledBy,
|
|
276
|
+
importantForAccessibility: this.importantForAccessibility,
|
|
277
|
+
accessibilityLiveRegion: this.accessibilityLiveRegion,
|
|
278
|
+
screenReaderFocusable: this.screenReaderFocusable,
|
|
279
|
+
accessibilityViewIsModal: this.accessibilityViewIsModal,
|
|
280
|
+
accessibilityElementsHidden: this.accessibilityElementsHidden,
|
|
281
|
+
accessibilityIgnoresInvertColors: this.accessibilityIgnoresInvertColors,
|
|
282
|
+
accessibilityLanguage: this.accessibilityLanguage,
|
|
283
|
+
accessibilityRespondsToUserInteraction: this.accessibilityRespondsToUserInteraction,
|
|
284
|
+
accessibilityShowsLargeContentViewer: this.accessibilityShowsLargeContentViewer,
|
|
285
|
+
accessibilityLargeContentTitle: this.accessibilityLargeContentTitle,
|
|
286
|
+
role: this.role,
|
|
287
|
+
'aria-label': this.ariaLabel,
|
|
288
|
+
'aria-labelledby': this.ariaLabelledBy,
|
|
289
|
+
'aria-live': this.ariaLive,
|
|
290
|
+
'aria-hidden': this.ariaHidden,
|
|
291
|
+
'aria-busy': this.ariaBusy,
|
|
292
|
+
'aria-checked': this.ariaChecked,
|
|
293
|
+
'aria-disabled': this.ariaDisabled,
|
|
294
|
+
'aria-expanded': this.ariaExpanded,
|
|
295
|
+
'aria-selected': this.ariaSelected,
|
|
296
|
+
'aria-modal': this.ariaModal,
|
|
297
|
+
'aria-valuemax': this.ariaValueMax,
|
|
298
|
+
'aria-valuemin': this.ariaValueMin,
|
|
299
|
+
'aria-valuenow': this.ariaValueNow,
|
|
300
|
+
'aria-valuetext': this.ariaValueText,
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import '@angular/compiler';
|
|
2
|
+
import { Component } from '@angular/core';
|
|
3
|
+
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
|
4
|
+
import { clearGlobalStyles, registerStyles } from '@symbiote-native/engine';
|
|
5
|
+
import { installFabric } from '@symbiote-native/test-utils';
|
|
6
|
+
|
|
7
|
+
import { mount, unmount } from '../../render';
|
|
8
|
+
import { KeyboardAvoidingView } from './index';
|
|
9
|
+
|
|
10
|
+
const ROOT_TAG = 911;
|
|
11
|
+
const fabric = installFabric();
|
|
12
|
+
|
|
13
|
+
// KeyboardAvoidingView's ngOnInit subscribes to the Keyboard module, which installs the
|
|
14
|
+
// bridgeless device-event hub on first use — needs a fake RN$registerCallableModule so
|
|
15
|
+
// installDeviceEventHub() doesn't throw (core/engine/src/native-events.ts). No native event is
|
|
16
|
+
// actually emitted in this test, only the subscribe/unsubscribe lifecycle runs.
|
|
17
|
+
Object.assign(globalThis, {
|
|
18
|
+
RN$registerCallableModule: (
|
|
19
|
+
_name: string,
|
|
20
|
+
_factory: () => { emit: (eventType: string, ...args: unknown[]) => void },
|
|
21
|
+
): void => {},
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
@Component({
|
|
25
|
+
selector: 'symbiote-kav-host',
|
|
26
|
+
standalone: true,
|
|
27
|
+
imports: [KeyboardAvoidingView],
|
|
28
|
+
template: `
|
|
29
|
+
<KeyboardAvoidingView [testID]="'kav'" class="panel">
|
|
30
|
+
<symbiote-text>Hello</symbiote-text>
|
|
31
|
+
</KeyboardAvoidingView>
|
|
32
|
+
`,
|
|
33
|
+
})
|
|
34
|
+
class KeyboardAvoidingViewHostFixture {}
|
|
35
|
+
|
|
36
|
+
beforeEach(() => {
|
|
37
|
+
fabric.reset();
|
|
38
|
+
});
|
|
39
|
+
afterEach(() => {
|
|
40
|
+
unmount(ROOT_TAG);
|
|
41
|
+
clearGlobalStyles();
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
describe('KeyboardAvoidingView', () => {
|
|
45
|
+
it('resolves a class= on the KeyboardAvoidingView use site onto the real committed view, not the anchor', async () => {
|
|
46
|
+
registerStyles({ panel: { backgroundColor: 'teal' } });
|
|
47
|
+
|
|
48
|
+
mount(ROOT_TAG, KeyboardAvoidingViewHostFixture);
|
|
49
|
+
await new Promise<void>(resolve => setTimeout(resolve, 0));
|
|
50
|
+
|
|
51
|
+
const node = fabric.find(n => n.props.testID === 'kav');
|
|
52
|
+
expect(node?.props.backgroundColor).toBe('teal');
|
|
53
|
+
});
|
|
54
|
+
});
|
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
// Modal: the Angular lifecycle half. RCTModalHostView is an ordinary Fabric host node committing
|
|
2
|
+
// through the same childSet as the rest of the tree (no second JS surface). The style math (the
|
|
3
|
+
// backdrop override, the container/host styles, the presentationStyle default), the visible gate,
|
|
4
|
+
// and the iOS keep-alive reducer all live framework-agnostic in @symbiote-native/components and are shared
|
|
5
|
+
// verbatim with React/Vue; here Angular supplies only the lifecycle: the keep-alive state + a
|
|
6
|
+
// POST-render transition (ngOnChanges queues the reducer on a microtask so it runs AFTER the render
|
|
7
|
+
// that used the OLD state, the Angular twin of React's useEffect / Vue's post-flush watch — one
|
|
8
|
+
// keep-alive frame survives the visible→hidden transition), reusing renderModal's resolved props.
|
|
9
|
+
// The user children nest UNDER the container View via <ng-content>.
|
|
10
|
+
|
|
11
|
+
import {
|
|
12
|
+
CUSTOM_ELEMENTS_SCHEMA,
|
|
13
|
+
ChangeDetectionStrategy,
|
|
14
|
+
ChangeDetectorRef,
|
|
15
|
+
Component,
|
|
16
|
+
ElementRef,
|
|
17
|
+
EventEmitter,
|
|
18
|
+
inject,
|
|
19
|
+
Input,
|
|
20
|
+
Output,
|
|
21
|
+
type OnChanges,
|
|
22
|
+
type OnInit,
|
|
23
|
+
type SimpleChanges,
|
|
24
|
+
} from '@angular/core';
|
|
25
|
+
import {
|
|
26
|
+
createInitialModalState,
|
|
27
|
+
modalReducer,
|
|
28
|
+
renderModal,
|
|
29
|
+
resolveAccessibilityProps,
|
|
30
|
+
shouldRenderModal,
|
|
31
|
+
type IAccessibilityProps,
|
|
32
|
+
type IAccessibilityStateValue,
|
|
33
|
+
type IAriaProps,
|
|
34
|
+
type IModalAnimationType,
|
|
35
|
+
type IModalOrientation,
|
|
36
|
+
type IModalOrientationChangeEvent,
|
|
37
|
+
type IModalPresentationStyle,
|
|
38
|
+
type IModalState,
|
|
39
|
+
} from '@symbiote-native/components';
|
|
40
|
+
import {
|
|
41
|
+
dlog,
|
|
42
|
+
isSymbioteEvent,
|
|
43
|
+
type IStyleProp,
|
|
44
|
+
type ISymbioteEvent,
|
|
45
|
+
type IViewStyle,
|
|
46
|
+
} from '@symbiote-native/engine';
|
|
47
|
+
import { anchorHostStyle, ModalHost, SymbioteHostPropsDirective, ViewHost } from '../../primitives';
|
|
48
|
+
|
|
49
|
+
export type {
|
|
50
|
+
IModalAnimationType,
|
|
51
|
+
IModalPresentationStyle,
|
|
52
|
+
IModalOrientation,
|
|
53
|
+
IModalOrientationChangeEvent,
|
|
54
|
+
} from '@symbiote-native/components';
|
|
55
|
+
|
|
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
|
+
// Mirrors React's IModalProps minus children (Angular takes children via <ng-content>).
|
|
68
|
+
export interface IAngularModalProps extends IAccessibilityProps, IAriaProps {
|
|
69
|
+
visible?: boolean;
|
|
70
|
+
transparent?: boolean;
|
|
71
|
+
backdropColor?: string;
|
|
72
|
+
animationType?: IModalAnimationType;
|
|
73
|
+
presentationStyle?: IModalPresentationStyle;
|
|
74
|
+
supportedOrientations?: ReadonlyArray<IModalOrientation>;
|
|
75
|
+
hardwareAccelerated?: boolean;
|
|
76
|
+
statusBarTranslucent?: boolean;
|
|
77
|
+
navigationBarTranslucent?: boolean;
|
|
78
|
+
allowSwipeDismissal?: boolean;
|
|
79
|
+
onShow?: () => void;
|
|
80
|
+
onDismiss?: () => void;
|
|
81
|
+
onRequestClose?: () => void;
|
|
82
|
+
onOrientationChange?: (event: IModalOrientationChangeEvent) => void;
|
|
83
|
+
style?: IStyleProp<IViewStyle>;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// What the Modal component itself takes as plain @Input()s: the full surface minus the
|
|
87
|
+
// show/dismiss/close/orientation and accessibility events, which it exposes as real @Output()
|
|
88
|
+
// EventEmitters instead (see the class below), mirroring the Pressable family's onPress -> press
|
|
89
|
+
// conversion (adapters/angular/src/components/pressable/index.ts).
|
|
90
|
+
export type IAngularModalInputs = Omit<
|
|
91
|
+
IAngularModalProps,
|
|
92
|
+
| 'onShow'
|
|
93
|
+
| 'onDismiss'
|
|
94
|
+
| 'onRequestClose'
|
|
95
|
+
| 'onOrientationChange'
|
|
96
|
+
| 'onAccessibilityAction'
|
|
97
|
+
| 'onAccessibilityTap'
|
|
98
|
+
| 'onMagicTap'
|
|
99
|
+
| 'onAccessibilityEscape'
|
|
100
|
+
>;
|
|
101
|
+
|
|
102
|
+
@Component({
|
|
103
|
+
selector: 'Modal',
|
|
104
|
+
standalone: true,
|
|
105
|
+
schemas: [CUSTOM_ELEMENTS_SCHEMA],
|
|
106
|
+
imports: [ModalHost, ViewHost, SymbioteHostPropsDirective],
|
|
107
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
108
|
+
template: `
|
|
109
|
+
@if (shouldRender) {
|
|
110
|
+
<symbiote-modal
|
|
111
|
+
[symbioteHostProps]="hostProps"
|
|
112
|
+
(show)="show.emit()"
|
|
113
|
+
(dismiss)="dismiss.emit()"
|
|
114
|
+
(requestClose)="requestClose.emit()"
|
|
115
|
+
(orientationChange)="emitOrientation($event)"
|
|
116
|
+
(accessibilityAction)="emit(accessibilityAction, $event)"
|
|
117
|
+
(accessibilityTap)="emit(accessibilityTap, $event)"
|
|
118
|
+
(magicTap)="emit(magicTap, $event)"
|
|
119
|
+
(accessibilityEscape)="emit(accessibilityEscape, $event)"
|
|
120
|
+
>
|
|
121
|
+
<symbiote-view [style]="containerStyle" [collapsable]="false">
|
|
122
|
+
<ng-content></ng-content>
|
|
123
|
+
</symbiote-view>
|
|
124
|
+
</symbiote-modal>
|
|
125
|
+
}
|
|
126
|
+
`,
|
|
127
|
+
})
|
|
128
|
+
export class Modal implements IAngularModalInputs, OnInit, OnChanges {
|
|
129
|
+
@Output() readonly show = new EventEmitter<void>();
|
|
130
|
+
@Output() readonly dismiss = new EventEmitter<void>();
|
|
131
|
+
@Output() readonly requestClose = new EventEmitter<void>();
|
|
132
|
+
@Output() readonly orientationChange = new EventEmitter<IModalOrientationChangeEvent>();
|
|
133
|
+
@Output() readonly accessibilityAction = new EventEmitter<ISymbioteEvent>();
|
|
134
|
+
@Output() readonly accessibilityTap = new EventEmitter<ISymbioteEvent>();
|
|
135
|
+
@Output() readonly magicTap = new EventEmitter<ISymbioteEvent>();
|
|
136
|
+
@Output() readonly accessibilityEscape = new EventEmitter<ISymbioteEvent>();
|
|
137
|
+
@Input() visible?: boolean;
|
|
138
|
+
@Input() transparent?: boolean;
|
|
139
|
+
@Input() backdropColor?: string;
|
|
140
|
+
@Input() animationType?: IModalAnimationType;
|
|
141
|
+
@Input() presentationStyle?: IModalPresentationStyle;
|
|
142
|
+
@Input() supportedOrientations?: ReadonlyArray<IModalOrientation>;
|
|
143
|
+
@Input() hardwareAccelerated?: boolean;
|
|
144
|
+
@Input() statusBarTranslucent?: boolean;
|
|
145
|
+
@Input() navigationBarTranslucent?: boolean;
|
|
146
|
+
@Input() allowSwipeDismissal?: boolean;
|
|
147
|
+
@Input() style?: IStyleProp<IViewStyle>;
|
|
148
|
+
@Input() testID?: string;
|
|
149
|
+
@Input() accessible?: boolean;
|
|
150
|
+
@Input() accessibilityLabel?: string;
|
|
151
|
+
@Input() accessibilityHint?: string;
|
|
152
|
+
@Input() accessibilityRole?: IAccessibilityProps['accessibilityRole'];
|
|
153
|
+
@Input() accessibilityState?: IAccessibilityStateValue;
|
|
154
|
+
@Input() accessibilityValue?: IAccessibilityProps['accessibilityValue'];
|
|
155
|
+
@Input() accessibilityActions?: IAccessibilityProps['accessibilityActions'];
|
|
156
|
+
@Input() accessibilityLabelledBy?: string | string[];
|
|
157
|
+
@Input() importantForAccessibility?: IAccessibilityProps['importantForAccessibility'];
|
|
158
|
+
@Input() accessibilityLiveRegion?: IAccessibilityProps['accessibilityLiveRegion'];
|
|
159
|
+
@Input() screenReaderFocusable?: boolean;
|
|
160
|
+
@Input() accessibilityViewIsModal?: boolean;
|
|
161
|
+
@Input() accessibilityElementsHidden?: boolean;
|
|
162
|
+
@Input() accessibilityIgnoresInvertColors?: boolean;
|
|
163
|
+
@Input() accessibilityLanguage?: string;
|
|
164
|
+
@Input() accessibilityRespondsToUserInteraction?: boolean;
|
|
165
|
+
@Input() accessibilityShowsLargeContentViewer?: boolean;
|
|
166
|
+
@Input() accessibilityLargeContentTitle?: string;
|
|
167
|
+
@Input() role?: IAriaProps['role'];
|
|
168
|
+
@Input('aria-label') ariaLabel?: string;
|
|
169
|
+
@Input('aria-labelledby') ariaLabelledBy?: string;
|
|
170
|
+
@Input('aria-live') ariaLive?: IAriaProps['aria-live'];
|
|
171
|
+
@Input('aria-hidden') ariaHidden?: boolean;
|
|
172
|
+
@Input('aria-busy') ariaBusy?: boolean;
|
|
173
|
+
@Input('aria-checked') ariaChecked?: boolean | 'mixed';
|
|
174
|
+
@Input('aria-disabled') ariaDisabled?: boolean;
|
|
175
|
+
@Input('aria-expanded') ariaExpanded?: boolean;
|
|
176
|
+
@Input('aria-selected') ariaSelected?: boolean;
|
|
177
|
+
@Input('aria-modal') ariaModal?: boolean;
|
|
178
|
+
@Input('aria-valuemax') ariaValueMax?: number;
|
|
179
|
+
@Input('aria-valuemin') ariaValueMin?: number;
|
|
180
|
+
@Input('aria-valuenow') ariaValueNow?: number;
|
|
181
|
+
@Input('aria-valuetext') ariaValueText?: string;
|
|
182
|
+
|
|
183
|
+
// The iOS keep-alive (state/modal.ts): on visible→hidden the node renders one more frame
|
|
184
|
+
// (isRendered still true) before unmounting, so the native onDismiss can arrive.
|
|
185
|
+
private state: IModalState = createInitialModalState(false);
|
|
186
|
+
|
|
187
|
+
private readonly changeDetector = inject(ChangeDetectorRef);
|
|
188
|
+
// This component's OWN host — the non-painting anchor `class="..."` at the use site resolves
|
|
189
|
+
// onto (see anchorHostStyle's doc comment) — NOT the inner `symbiote-view [style]="containerStyle"`
|
|
190
|
+
// that hosts the user children; the outer `symbiote-modal` node (bound via `hostProps` below) is
|
|
191
|
+
// the real primitive the anchor sits in front of.
|
|
192
|
+
private readonly elementRef = inject(ElementRef);
|
|
193
|
+
|
|
194
|
+
ngOnInit(): void {
|
|
195
|
+
this.state = createInitialModalState(this.visible === true);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
ngOnChanges(changes: SimpleChanges): void {
|
|
199
|
+
const visibleChange = changes.visible;
|
|
200
|
+
// First change is reflected by the ngOnInit seed; only later toggles drive the keep-alive.
|
|
201
|
+
if (visibleChange === undefined || visibleChange.firstChange) return;
|
|
202
|
+
const isVisible = this.visible === true;
|
|
203
|
+
// Queue on a microtask so the reducer runs AFTER this CD pass renders with the OLD state —
|
|
204
|
+
// the keep-alive frame. A synchronous dispatch here would unmount in the same pass.
|
|
205
|
+
queueMicrotask(() => {
|
|
206
|
+
this.state = modalReducer(this.state, isVisible ? { type: 'show' } : { type: 'hide' });
|
|
207
|
+
this.changeDetector.markForCheck();
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
get shouldRender(): boolean {
|
|
212
|
+
const render = shouldRenderModal(this.visible === true, this.state);
|
|
213
|
+
if (!render) dlog('Modal hidden -> no node committed');
|
|
214
|
+
return render;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// renderModal owns the backdrop/presentationStyle/style math (shared with React/Vue); the
|
|
218
|
+
// adapter reads the resolved host + container props off the Descriptor it returns.
|
|
219
|
+
private get descriptor() {
|
|
220
|
+
return renderModal({
|
|
221
|
+
visible: this.visible,
|
|
222
|
+
transparent: this.transparent,
|
|
223
|
+
backdropColor: this.backdropColor,
|
|
224
|
+
animationType: this.animationType,
|
|
225
|
+
presentationStyle: this.presentationStyle,
|
|
226
|
+
supportedOrientations: this.supportedOrientations,
|
|
227
|
+
hardwareAccelerated: this.hardwareAccelerated,
|
|
228
|
+
statusBarTranslucent: this.statusBarTranslucent,
|
|
229
|
+
navigationBarTranslucent: this.navigationBarTranslucent,
|
|
230
|
+
allowSwipeDismissal: this.allowSwipeDismissal,
|
|
231
|
+
style: this.style,
|
|
232
|
+
passthrough: resolveAccessibilityProps(this.accessibilityInputs()),
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// renderModal's own MODAL_HOST_STYLE (position:'absolute') is the outer symbiote-modal node's
|
|
237
|
+
// style; the anchor's class-derived style goes FIRST, that resolved style SECOND —
|
|
238
|
+
// flattenStyle's later-wins collapse keeps the modal's own style winning over its ambient class.
|
|
239
|
+
get hostProps(): Record<string, unknown> {
|
|
240
|
+
const descriptorProps = this.descriptor.props;
|
|
241
|
+
return { ...descriptorProps, style: [anchorHostStyle(this.elementRef), descriptorProps.style] };
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
get containerStyle(): unknown {
|
|
245
|
+
const container = this.descriptor.children[0];
|
|
246
|
+
return typeof container === 'string' ? undefined : container.props.style;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
emit(emitter: EventEmitter<ISymbioteEvent>, event: unknown): void {
|
|
250
|
+
if (isSymbioteEvent(event)) emitter.emit(event);
|
|
251
|
+
}
|
|
252
|
+
|
|
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
|
+
// Typed as the a11y intersection WITH the string index (the bag renderModal spreads into the
|
|
260
|
+
// host props), so resolveAccessibilityProps's result stays assignable to passthrough.
|
|
261
|
+
private accessibilityInputs(): IAccessibilityProps & IAriaProps & Record<string, unknown> {
|
|
262
|
+
return {
|
|
263
|
+
testID: this.testID,
|
|
264
|
+
accessible: this.accessible,
|
|
265
|
+
accessibilityLabel: this.accessibilityLabel,
|
|
266
|
+
accessibilityHint: this.accessibilityHint,
|
|
267
|
+
accessibilityRole: this.accessibilityRole,
|
|
268
|
+
accessibilityState: this.accessibilityState,
|
|
269
|
+
accessibilityValue: this.accessibilityValue,
|
|
270
|
+
accessibilityActions: this.accessibilityActions,
|
|
271
|
+
accessibilityLabelledBy: this.accessibilityLabelledBy,
|
|
272
|
+
importantForAccessibility: this.importantForAccessibility,
|
|
273
|
+
accessibilityLiveRegion: this.accessibilityLiveRegion,
|
|
274
|
+
screenReaderFocusable: this.screenReaderFocusable,
|
|
275
|
+
accessibilityViewIsModal: this.accessibilityViewIsModal,
|
|
276
|
+
accessibilityElementsHidden: this.accessibilityElementsHidden,
|
|
277
|
+
accessibilityIgnoresInvertColors: this.accessibilityIgnoresInvertColors,
|
|
278
|
+
accessibilityLanguage: this.accessibilityLanguage,
|
|
279
|
+
accessibilityRespondsToUserInteraction: this.accessibilityRespondsToUserInteraction,
|
|
280
|
+
accessibilityShowsLargeContentViewer: this.accessibilityShowsLargeContentViewer,
|
|
281
|
+
accessibilityLargeContentTitle: this.accessibilityLargeContentTitle,
|
|
282
|
+
role: this.role,
|
|
283
|
+
'aria-label': this.ariaLabel,
|
|
284
|
+
'aria-labelledby': this.ariaLabelledBy,
|
|
285
|
+
'aria-live': this.ariaLive,
|
|
286
|
+
'aria-hidden': this.ariaHidden,
|
|
287
|
+
'aria-busy': this.ariaBusy,
|
|
288
|
+
'aria-checked': this.ariaChecked,
|
|
289
|
+
'aria-disabled': this.ariaDisabled,
|
|
290
|
+
'aria-expanded': this.ariaExpanded,
|
|
291
|
+
'aria-selected': this.ariaSelected,
|
|
292
|
+
'aria-modal': this.ariaModal,
|
|
293
|
+
'aria-valuemax': this.ariaValueMax,
|
|
294
|
+
'aria-valuemin': this.ariaValueMin,
|
|
295
|
+
'aria-valuenow': this.ariaValueNow,
|
|
296
|
+
'aria-valuetext': this.ariaValueText,
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
}
|