@symbiote-native/angular 0.8.0 → 1.0.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/babel-lower-host-primitives.cjs +636 -0
- package/babel-register-composed.cjs +21 -3
- package/build/angular/anchor-host-registry.js +3 -1
- package/build/angular/anchor-host-registry.js.map +1 -1
- package/build/angular/components/button.d.ts +3 -0
- package/build/angular/components/button.js +10 -1
- package/build/angular/components/button.js.map +1 -1
- package/build/angular/components/image/index.android.js +0 -8
- package/build/angular/components/image/index.android.js.map +1 -1
- package/build/angular/components/image/index.ios.js +0 -8
- package/build/angular/components/image/index.ios.js.map +1 -1
- package/build/angular/components/image/shared.d.ts +4 -4
- package/build/angular/components/image/shared.js +33 -27
- package/build/angular/components/image/shared.js.map +1 -1
- package/build/angular/components/image-background.d.ts +3 -0
- package/build/angular/components/image-background.js +10 -1
- package/build/angular/components/image-background.js.map +1 -1
- package/build/angular/components/input-accessory-view/index.d.ts +2 -1
- package/build/angular/components/input-accessory-view/index.js +11 -14
- package/build/angular/components/input-accessory-view/index.js.map +1 -1
- package/build/angular/components/keyboard-avoiding-view/index.d.ts +2 -1
- package/build/angular/components/keyboard-avoiding-view/index.js +17 -10
- package/build/angular/components/keyboard-avoiding-view/index.js.map +1 -1
- package/build/angular/components/modal/index.d.ts +1 -0
- package/build/angular/components/modal/index.js +14 -8
- package/build/angular/components/modal/index.js.map +1 -1
- package/build/angular/components/pressable/index.d.ts +9 -4
- package/build/angular/components/pressable/index.js +55 -14
- package/build/angular/components/pressable/index.js.map +1 -1
- package/build/angular/components/refresh-control/index.d.ts +2 -1
- package/build/angular/components/refresh-control/index.js +10 -8
- package/build/angular/components/refresh-control/index.js.map +1 -1
- package/build/angular/components/safe-area-view/index.d.ts +3 -1
- package/build/angular/components/safe-area-view/index.js +19 -19
- package/build/angular/components/safe-area-view/index.js.map +1 -1
- package/build/angular/components/scroll-view/shared.d.ts +2 -0
- package/build/angular/components/scroll-view/shared.js +20 -4
- package/build/angular/components/scroll-view/shared.js.map +1 -1
- package/build/angular/components/section-list/index.d.ts +3 -0
- package/build/angular/components/section-list/index.js +10 -1
- package/build/angular/components/section-list/index.js.map +1 -1
- package/build/angular/components/switch/index.android.js +6 -6
- package/build/angular/components/switch/index.android.js.map +1 -1
- package/build/angular/components/switch/index.ios.js +6 -6
- package/build/angular/components/switch/index.ios.js.map +1 -1
- package/build/angular/components/text-input.d.ts +7 -1
- package/build/angular/components/text-input.js +54 -29
- package/build/angular/components/text-input.js.map +1 -1
- package/build/angular/components/touchable/index.d.ts +7 -0
- package/build/angular/components/touchable/index.js +34 -3
- package/build/angular/components/touchable/index.js.map +1 -1
- package/build/angular/components/touchable-native-feedback/index.d.ts +2 -1
- package/build/angular/components/touchable-native-feedback/index.js +17 -15
- package/build/angular/components/touchable-native-feedback/index.js.map +1 -1
- package/build/angular/components/virtualized-list/index.d.ts +3 -0
- package/build/angular/components/virtualized-list/index.js +10 -1
- package/build/angular/components/virtualized-list/index.js.map +1 -1
- package/build/angular/components/virtualized-section-list/index.d.ts +3 -0
- package/build/angular/components/virtualized-section-list/index.js +10 -1
- package/build/angular/components/virtualized-section-list/index.js.map +1 -1
- package/build/angular/components.d.ts +1 -1
- package/build/angular/components.js +1 -1
- package/build/angular/components.js.map +1 -1
- package/build/angular/create-portal/index.d.ts +11 -2
- package/build/angular/create-portal/index.js +37 -11
- package/build/angular/create-portal/index.js.map +1 -1
- package/build/angular/gate-demand.d.ts +37 -0
- package/build/angular/gate-demand.js +84 -0
- package/build/angular/gate-demand.js.map +1 -0
- package/build/angular/index.d.ts +1 -0
- package/build/angular/index.js +1 -0
- package/build/angular/index.js.map +1 -1
- package/build/angular/modules/animated/create-animated-component.d.ts +1 -0
- package/build/angular/modules/animated/create-animated-component.js +14 -9
- package/build/angular/modules/animated/create-animated-component.js.map +1 -1
- package/build/angular/primitives/index.d.ts +13 -1
- package/build/angular/primitives/index.js +43 -1
- package/build/angular/primitives/index.js.map +1 -1
- package/build/angular/register.d.ts +1 -0
- package/build/angular/register.js +35 -0
- package/build/angular/register.js.map +1 -0
- package/build/angular/render/index.js +3 -1
- package/build/angular/render/index.js.map +1 -1
- package/build/angular/renderer/index.js +120 -19
- package/build/angular/renderer/index.js.map +1 -1
- package/build/angular/state-style.d.ts +9 -0
- package/build/angular/state-style.js +39 -0
- package/build/angular/state-style.js.map +1 -0
- package/metro-transformer.cjs +69 -0
- package/package.json +27 -8
- package/src/anchor-host-registry.ts +3 -1
- package/src/components/button.ts +17 -0
- package/src/components/image/index.android.ts +0 -4
- package/src/components/image/index.ios.ts +0 -4
- package/src/components/image/shared.ts +52 -26
- package/src/components/image-background.ts +18 -0
- package/src/components/input-accessory-view/index.ts +14 -8
- package/src/components/keyboard-avoiding-view/index.ts +21 -7
- package/src/components/modal/index.ts +17 -4
- package/src/components/pressable/index.ts +70 -10
- package/src/components/refresh-control/index.ts +14 -5
- package/src/components/safe-area-view/index.ts +28 -12
- package/src/components/scroll-view/shared.ts +31 -4
- package/src/components/section-list/index.ts +18 -0
- package/src/components/switch/index.android.ts +4 -4
- package/src/components/switch/index.ios.ts +4 -4
- package/src/components/text-input.ts +73 -17
- package/src/components/touchable/index.ts +43 -0
- package/src/components/touchable-native-feedback/index.ts +18 -8
- package/src/components/virtualized-list/index.ts +18 -0
- package/src/components/virtualized-section-list/index.ts +18 -0
- package/src/components.ts +2 -0
- package/src/create-portal/index.ts +39 -9
- package/src/gate-demand.ts +109 -0
- package/src/index.ts +2 -0
- package/src/modules/animated/create-animated-component.ts +23 -4
- package/src/primitives/index.ts +28 -1
- package/src/register.ts +43 -0
- package/src/render/index.ts +3 -1
- package/src/renderer/index.ts +144 -34
- package/src/state-style.ts +39 -0
|
@@ -100,6 +100,13 @@ export {
|
|
|
100
100
|
VListHeaderDirective,
|
|
101
101
|
VListSeparatorDirective,
|
|
102
102
|
} from '../virtualized-list';
|
|
103
|
+
import {
|
|
104
|
+
gateWanted,
|
|
105
|
+
injectGateDemandAbove,
|
|
106
|
+
provideGateDemand,
|
|
107
|
+
type IGateDemand,
|
|
108
|
+
type IGatedAccessibilityEvent,
|
|
109
|
+
} from '../../gate-demand';
|
|
103
110
|
|
|
104
111
|
// SectionList's public surface is exactly VirtualizedSectionList's (RN layers them one-for-one), so
|
|
105
112
|
// the prop type is shared verbatim — no SectionList-only field exists.
|
|
@@ -123,6 +130,7 @@ export type ISectionListInputs<ItemT> = Omit<
|
|
|
123
130
|
@Component({
|
|
124
131
|
selector: 'SectionList',
|
|
125
132
|
standalone: true,
|
|
133
|
+
viewProviders: [provideGateDemand(() => SectionList)],
|
|
126
134
|
hostDirectives: [
|
|
127
135
|
{ directive: SymbioteStyleInputDirective, inputs: ['style'] },
|
|
128
136
|
],
|
|
@@ -381,6 +389,16 @@ export class SectionList<ItemT = unknown>
|
|
|
381
389
|
@Output() readonly accessibilityTap = new EventEmitter<ISymbioteEvent>();
|
|
382
390
|
@Output() readonly magicTap = new EventEmitter<ISymbioteEvent>();
|
|
383
391
|
@Output() readonly accessibilityEscape = new EventEmitter<ISymbioteEvent>();
|
|
392
|
+
|
|
393
|
+
// This wrapper binds the four gated accessibility events on the component it renders, which
|
|
394
|
+
// Angular forces to be unconditional and which would light that component's gates on every
|
|
395
|
+
// instance. It answers for them instead — see `gate-demand.ts`.
|
|
396
|
+
private readonly gateDemandAbove = injectGateDemandAbove();
|
|
397
|
+
|
|
398
|
+
wantsGate(name: IGatedAccessibilityEvent): boolean {
|
|
399
|
+
return gateWanted(this.gateDemandAbove, name, this[name]);
|
|
400
|
+
}
|
|
401
|
+
|
|
384
402
|
@Input() role?: IAriaProps['role'];
|
|
385
403
|
@Input() ariaLabel?: string;
|
|
386
404
|
@Input() ariaLabelledBy?: string;
|
|
@@ -10,7 +10,7 @@ import {
|
|
|
10
10
|
import { NG_VALUE_ACCESSOR } from '@angular/forms';
|
|
11
11
|
import {
|
|
12
12
|
anchorHostStyle,
|
|
13
|
-
|
|
13
|
+
ManagedSwitchHost,
|
|
14
14
|
SymbioteHostPropsDirective,
|
|
15
15
|
SymbioteStyleInputDirective,
|
|
16
16
|
} from '../../primitives';
|
|
@@ -24,7 +24,7 @@ export type { ISwitchProps, ISwitchTrackColor } from './shared';
|
|
|
24
24
|
{ directive: SymbioteStyleInputDirective, inputs: ['style'] },
|
|
25
25
|
],
|
|
26
26
|
schemas: [CUSTOM_ELEMENTS_SCHEMA],
|
|
27
|
-
imports: [
|
|
27
|
+
imports: [ManagedSwitchHost, SymbioteHostPropsDirective],
|
|
28
28
|
providers: [
|
|
29
29
|
{
|
|
30
30
|
provide: NG_VALUE_ACCESSOR,
|
|
@@ -54,7 +54,7 @@ export type { ISwitchProps, ISwitchTrackColor } from './shared';
|
|
|
54
54
|
],
|
|
55
55
|
outputs: ['valueChange', 'change'],
|
|
56
56
|
template: `
|
|
57
|
-
<symbiote-switch
|
|
57
|
+
<symbiote-switch-managed
|
|
58
58
|
#nativeSwitch="symbioteHost"
|
|
59
59
|
[symbioteHostProps]="hostProps()"
|
|
60
60
|
(change)="handleChange($event, nativeSwitch)"
|
|
@@ -63,7 +63,7 @@ export type { ISwitchProps, ISwitchTrackColor } from './shared';
|
|
|
63
63
|
})
|
|
64
64
|
export class Switch extends SwitchBase implements DoCheck {
|
|
65
65
|
// This component's OWN host — the non-painting anchor `class="..."` at the use site resolves
|
|
66
|
-
// onto (see anchorHostStyle's doc comment) — NOT the <symbiote-switch> leaf one level down.
|
|
66
|
+
// onto (see anchorHostStyle's doc comment) — NOT the <symbiote-switch-managed> leaf one level down.
|
|
67
67
|
private readonly elementRef = inject(ElementRef);
|
|
68
68
|
|
|
69
69
|
protected readonly platform = {
|
|
@@ -10,7 +10,7 @@ import {
|
|
|
10
10
|
import { NG_VALUE_ACCESSOR } from '@angular/forms';
|
|
11
11
|
import {
|
|
12
12
|
anchorHostStyle,
|
|
13
|
-
|
|
13
|
+
ManagedSwitchHost,
|
|
14
14
|
SymbioteHostPropsDirective,
|
|
15
15
|
SymbioteStyleInputDirective,
|
|
16
16
|
} from '../../primitives';
|
|
@@ -24,7 +24,7 @@ export type { ISwitchProps, ISwitchTrackColor } from './shared';
|
|
|
24
24
|
{ directive: SymbioteStyleInputDirective, inputs: ['style'] },
|
|
25
25
|
],
|
|
26
26
|
schemas: [CUSTOM_ELEMENTS_SCHEMA],
|
|
27
|
-
imports: [
|
|
27
|
+
imports: [ManagedSwitchHost, SymbioteHostPropsDirective],
|
|
28
28
|
providers: [
|
|
29
29
|
{
|
|
30
30
|
provide: NG_VALUE_ACCESSOR,
|
|
@@ -54,7 +54,7 @@ export type { ISwitchProps, ISwitchTrackColor } from './shared';
|
|
|
54
54
|
],
|
|
55
55
|
outputs: ['valueChange', 'change'],
|
|
56
56
|
template: `
|
|
57
|
-
<symbiote-switch
|
|
57
|
+
<symbiote-switch-managed
|
|
58
58
|
#nativeSwitch="symbioteHost"
|
|
59
59
|
[symbioteHostProps]="hostProps()"
|
|
60
60
|
(change)="handleChange($event, nativeSwitch)"
|
|
@@ -63,7 +63,7 @@ export type { ISwitchProps, ISwitchTrackColor } from './shared';
|
|
|
63
63
|
})
|
|
64
64
|
export class Switch extends SwitchBase implements DoCheck {
|
|
65
65
|
// This component's OWN host — the non-painting anchor `class="..."` at the use site resolves
|
|
66
|
-
// onto (see anchorHostStyle's doc comment) — NOT the <symbiote-switch> leaf one level down.
|
|
66
|
+
// onto (see anchorHostStyle's doc comment) — NOT the <symbiote-switch-managed> leaf one level down.
|
|
67
67
|
private readonly elementRef = inject(ElementRef);
|
|
68
68
|
|
|
69
69
|
protected readonly platform = {
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// and the controlled-write predicate live in @symbiote-native/components/state, the render (intrinsic
|
|
3
3
|
// + native-prop mapping) in @symbiote-native/components/view, both shared verbatim with the React and
|
|
4
4
|
// Vue adapters. Here Angular supplies only the lifecycle: renderTextInput picks ONE of two host
|
|
5
|
-
// intrinsics at runtime (symbiote-text-input /
|
|
5
|
+
// intrinsics at runtime (symbiote-text-input-managed / …-multiline-managed) via `@if`/`@else`,
|
|
6
6
|
// each wired through the shared `SymbioteHostPropsDirective` (`[symbioteHostProps]="hostProps"`,
|
|
7
7
|
// `../primitives/shared.ts`) so the flat resolved prop bag doesn't need enumerating as individual
|
|
8
8
|
// `[prop]` bindings; a @ViewChild reading that directive (by its `#host` template ref, across
|
|
@@ -73,6 +73,9 @@ import {
|
|
|
73
73
|
setInputBlurred,
|
|
74
74
|
setInputFocused,
|
|
75
75
|
whenCommitted,
|
|
76
|
+
type IMeasureInWindowOnSuccess,
|
|
77
|
+
type IMeasureLayoutOnSuccess,
|
|
78
|
+
type IMeasureOnSuccess,
|
|
76
79
|
type IStyleProp,
|
|
77
80
|
type ISymbioteEvent,
|
|
78
81
|
type ISymbioteNode,
|
|
@@ -80,11 +83,16 @@ import {
|
|
|
80
83
|
} from '@symbiote-native/engine';
|
|
81
84
|
import {
|
|
82
85
|
anchorHostStyle,
|
|
83
|
-
|
|
86
|
+
ManagedMultilineTextInputHost,
|
|
84
87
|
SymbioteHostPropsDirective,
|
|
85
88
|
SymbioteStyleInputDirective,
|
|
86
|
-
|
|
89
|
+
ManagedTextInputHost,
|
|
87
90
|
} from '../primitives';
|
|
91
|
+
import {
|
|
92
|
+
gateWanted,
|
|
93
|
+
injectGateDemand,
|
|
94
|
+
type IGatedAccessibilityEvent,
|
|
95
|
+
} from '../gate-demand';
|
|
88
96
|
|
|
89
97
|
export type {
|
|
90
98
|
ITextInputHandle,
|
|
@@ -169,7 +177,11 @@ export type IAngularTextInputInputs = Omit<
|
|
|
169
177
|
{ directive: SymbioteStyleInputDirective, inputs: ['style'] },
|
|
170
178
|
],
|
|
171
179
|
schemas: [CUSTOM_ELEMENTS_SCHEMA],
|
|
172
|
-
imports: [
|
|
180
|
+
imports: [
|
|
181
|
+
SymbioteHostPropsDirective,
|
|
182
|
+
ManagedTextInputHost,
|
|
183
|
+
ManagedMultilineTextInputHost,
|
|
184
|
+
],
|
|
173
185
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
174
186
|
providers: [
|
|
175
187
|
{
|
|
@@ -180,7 +192,7 @@ export type IAngularTextInputInputs = Omit<
|
|
|
180
192
|
],
|
|
181
193
|
template: `
|
|
182
194
|
@if (isMultiline) {
|
|
183
|
-
<symbiote-text-input-multiline
|
|
195
|
+
<symbiote-text-input-multiline-managed
|
|
184
196
|
#host
|
|
185
197
|
[symbioteHostProps]="hostProps"
|
|
186
198
|
(change)="handleChange($event)"
|
|
@@ -191,13 +203,9 @@ export type IAngularTextInputInputs = Omit<
|
|
|
191
203
|
(keyPress)="emit(keyPress, $event)"
|
|
192
204
|
(endEditing)="emit(endEditing, $event)"
|
|
193
205
|
(contentSizeChange)="emit(contentSizeChange, $event)"
|
|
194
|
-
|
|
195
|
-
(accessibilityTap)="emit(accessibilityTap, $event)"
|
|
196
|
-
(magicTap)="emit(magicTap, $event)"
|
|
197
|
-
(accessibilityEscape)="emit(accessibilityEscape, $event)"
|
|
198
|
-
></symbiote-text-input-multiline>
|
|
206
|
+
></symbiote-text-input-multiline-managed>
|
|
199
207
|
} @else {
|
|
200
|
-
<symbiote-text-input
|
|
208
|
+
<symbiote-text-input-managed
|
|
201
209
|
#host
|
|
202
210
|
[symbioteHostProps]="hostProps"
|
|
203
211
|
(change)="handleChange($event)"
|
|
@@ -208,11 +216,7 @@ export type IAngularTextInputInputs = Omit<
|
|
|
208
216
|
(keyPress)="emit(keyPress, $event)"
|
|
209
217
|
(endEditing)="emit(endEditing, $event)"
|
|
210
218
|
(contentSizeChange)="emit(contentSizeChange, $event)"
|
|
211
|
-
|
|
212
|
-
(accessibilityTap)="emit(accessibilityTap, $event)"
|
|
213
|
-
(magicTap)="emit(magicTap, $event)"
|
|
214
|
-
(accessibilityEscape)="emit(accessibilityEscape, $event)"
|
|
215
|
-
></symbiote-text-input>
|
|
219
|
+
></symbiote-text-input-managed>
|
|
216
220
|
}
|
|
217
221
|
`,
|
|
218
222
|
})
|
|
@@ -282,6 +286,9 @@ export class TextInput
|
|
|
282
286
|
@Output() readonly magicTap = new EventEmitter<ISymbioteEvent>();
|
|
283
287
|
@Output() readonly accessibilityEscape = new EventEmitter<ISymbioteEvent>();
|
|
284
288
|
|
|
289
|
+
// Null unless a wrapper renders this TextInput in its own template. See `../gate-demand.ts`.
|
|
290
|
+
private readonly gateDemand = injectGateDemand();
|
|
291
|
+
|
|
285
292
|
@Input() accessible?: boolean;
|
|
286
293
|
@Input() accessibilityLabel?: string;
|
|
287
294
|
@Input() accessibilityHint?: string;
|
|
@@ -318,7 +325,7 @@ export class TextInput
|
|
|
318
325
|
|
|
319
326
|
// This component's OWN host — the non-painting anchor `class="..."` at the use site resolves
|
|
320
327
|
// onto (see anchorHostStyle's doc comment) — NOT `hostRef` above, which targets the real inner
|
|
321
|
-
// symbiote-text-input(-multiline) one level down.
|
|
328
|
+
// symbiote-text-input-managed(-multiline-managed) one level down.
|
|
322
329
|
private readonly elementRef = inject(ElementRef);
|
|
323
330
|
// Only setDisabledState() needs this: it mutates `editable` from OUTSIDE Angular's own binding
|
|
324
331
|
// path (@angular/forms calling in directly), which doesn't itself schedule a tick under zoneless CD.
|
|
@@ -458,6 +465,41 @@ export class TextInput
|
|
|
458
465
|
if (isSymbioteEvent(event)) emitter.emit(event);
|
|
459
466
|
}
|
|
460
467
|
|
|
468
|
+
// `undefined` means `setEventListener` never sees a function, so the gate stays dark. A demand
|
|
469
|
+
// from above overrides `.observed` for the same reason it does on Pressable: a wrapper's binding
|
|
470
|
+
// is not a subscriber (`../gate-demand.ts`).
|
|
471
|
+
private gatedAccessibilityHandler(
|
|
472
|
+
name: IGatedAccessibilityEvent,
|
|
473
|
+
): ((event: unknown) => void) | undefined {
|
|
474
|
+
const emitter = this[name];
|
|
475
|
+
return gateWanted(this.gateDemand, name, emitter)
|
|
476
|
+
? event => this.emit(emitter, event)
|
|
477
|
+
: undefined;
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
// Forwarded from the engine node so a TextInput ref is not poorer than any other host ref — an
|
|
481
|
+
// instance exposing only the five methods below CLOSES the node off (see ITextInputHandle's own
|
|
482
|
+
// comment). No Angular-specific state to add: these four pass straight through.
|
|
483
|
+
measure(callback: IMeasureOnSuccess): void {
|
|
484
|
+
this.hostNode?.measure(callback);
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
measureInWindow(callback: IMeasureInWindowOnSuccess): void {
|
|
488
|
+
this.hostNode?.measureInWindow(callback);
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
measureLayout(
|
|
492
|
+
relativeToNativeNode: ISymbioteNode | number,
|
|
493
|
+
onSuccess: IMeasureLayoutOnSuccess,
|
|
494
|
+
onFail?: () => void,
|
|
495
|
+
): void {
|
|
496
|
+
this.hostNode?.measureLayout(relativeToNativeNode, onSuccess, onFail);
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
setNativeProps(nativeProps: Record<string, unknown>): void {
|
|
500
|
+
this.hostNode?.setNativeProps(nativeProps);
|
|
501
|
+
}
|
|
502
|
+
|
|
461
503
|
// The imperative API RN exposes on a TextInput ref; in Angular the component instance IS the
|
|
462
504
|
// handle (a parent reads it via @ViewChild(TextInput) and calls these). focus/blur drive native
|
|
463
505
|
// view commands; clear and setSelection reuse setTextAndSelection (the same stale-safe path as a
|
|
@@ -531,6 +573,20 @@ export class TextInput
|
|
|
531
573
|
return {
|
|
532
574
|
...this.foldedAccessibility,
|
|
533
575
|
accessible: this.accessible,
|
|
576
|
+
// NOT template bindings, unlike every other event on this component's two host tags. These
|
|
577
|
+
// four are `on*`-GATED Fabric events (`.claude/rules/fabric-boolean-event-gates.md`): native
|
|
578
|
+
// fires them only when the committed payload carries a FUNCTION at that key, and a template
|
|
579
|
+
// `(accessibilityAction)="..."` binding is present unconditionally — so it lit all four gates
|
|
580
|
+
// on every TextInput in every app whether or not anything listened. Routed through the flat
|
|
581
|
+
// bag instead, the same shape Pressable uses.
|
|
582
|
+
onAccessibilityAction: this.gatedAccessibilityHandler(
|
|
583
|
+
'accessibilityAction',
|
|
584
|
+
),
|
|
585
|
+
onAccessibilityTap: this.gatedAccessibilityHandler('accessibilityTap'),
|
|
586
|
+
onMagicTap: this.gatedAccessibilityHandler('magicTap'),
|
|
587
|
+
onAccessibilityEscape: this.gatedAccessibilityHandler(
|
|
588
|
+
'accessibilityEscape',
|
|
589
|
+
),
|
|
534
590
|
testID: this.testID,
|
|
535
591
|
nativeID: this.nativeID,
|
|
536
592
|
placeholder: this.placeholder,
|
|
@@ -72,6 +72,13 @@ import {
|
|
|
72
72
|
} from '../../primitives';
|
|
73
73
|
import { Pressable, type IAngularPressableInputs } from '../pressable';
|
|
74
74
|
import { Animated, AnimatedView } from '../../modules/animated';
|
|
75
|
+
import {
|
|
76
|
+
gateWanted,
|
|
77
|
+
injectGateDemandAbove,
|
|
78
|
+
provideGateDemand,
|
|
79
|
+
type IGateDemand,
|
|
80
|
+
type IGatedAccessibilityEvent,
|
|
81
|
+
} from '../../gate-demand';
|
|
75
82
|
|
|
76
83
|
// The shared field base for all three: the Pressable INPUT surface (minus style, which each
|
|
77
84
|
// Touchable routes differently, and minus the press/hover events, which each Touchable declares as
|
|
@@ -128,6 +135,7 @@ function createTimerScheduler(): ITimerScheduler {
|
|
|
128
135
|
@Component({
|
|
129
136
|
selector: 'TouchableOpacity',
|
|
130
137
|
standalone: true,
|
|
138
|
+
viewProviders: [provideGateDemand(() => TouchableOpacity)],
|
|
131
139
|
hostDirectives: [
|
|
132
140
|
{ directive: SymbioteStyleInputDirective, inputs: ['style'] },
|
|
133
141
|
],
|
|
@@ -136,6 +144,7 @@ function createTimerScheduler(): ITimerScheduler {
|
|
|
136
144
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
137
145
|
template: `
|
|
138
146
|
<Pressable
|
|
147
|
+
[__minPressDuration]="0"
|
|
139
148
|
(press)="press.emit($event)"
|
|
140
149
|
(pressIn)="handlePressIn($event)"
|
|
141
150
|
(pressOut)="handlePressOut($event)"
|
|
@@ -224,6 +233,16 @@ export class TouchableOpacity
|
|
|
224
233
|
@Output() readonly accessibilityTap = new EventEmitter<ISymbioteEvent>();
|
|
225
234
|
@Output() readonly magicTap = new EventEmitter<ISymbioteEvent>();
|
|
226
235
|
@Output() readonly accessibilityEscape = new EventEmitter<ISymbioteEvent>();
|
|
236
|
+
|
|
237
|
+
// This wrapper binds the four gated accessibility events on the component it renders, which
|
|
238
|
+
// Angular forces to be unconditional and which would light that component's gates on every
|
|
239
|
+
// instance. It answers for them instead — see `gate-demand.ts`.
|
|
240
|
+
private readonly gateDemandAbove = injectGateDemandAbove();
|
|
241
|
+
|
|
242
|
+
wantsGate(name: IGatedAccessibilityEvent): boolean {
|
|
243
|
+
return gateWanted(this.gateDemandAbove, name, this[name]);
|
|
244
|
+
}
|
|
245
|
+
|
|
227
246
|
@Input() activeOpacity?: number;
|
|
228
247
|
@Input() delayPressIn?: number;
|
|
229
248
|
@Input() delayPressOut?: number;
|
|
@@ -417,6 +436,7 @@ export class TouchableOpacity
|
|
|
417
436
|
@Component({
|
|
418
437
|
selector: 'TouchableHighlight',
|
|
419
438
|
standalone: true,
|
|
439
|
+
viewProviders: [provideGateDemand(() => TouchableHighlight)],
|
|
420
440
|
hostDirectives: [
|
|
421
441
|
{ directive: SymbioteStyleInputDirective, inputs: ['style'] },
|
|
422
442
|
],
|
|
@@ -424,6 +444,7 @@ export class TouchableOpacity
|
|
|
424
444
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
425
445
|
template: `
|
|
426
446
|
<Pressable
|
|
447
|
+
[__minPressDuration]="0"
|
|
427
448
|
[style]="containerStyle()"
|
|
428
449
|
(press)="handlePress($event)"
|
|
429
450
|
(pressIn)="handlePressIn($event)"
|
|
@@ -511,6 +532,16 @@ export class TouchableHighlight
|
|
|
511
532
|
@Output() readonly accessibilityTap = new EventEmitter<ISymbioteEvent>();
|
|
512
533
|
@Output() readonly magicTap = new EventEmitter<ISymbioteEvent>();
|
|
513
534
|
@Output() readonly accessibilityEscape = new EventEmitter<ISymbioteEvent>();
|
|
535
|
+
|
|
536
|
+
// This wrapper binds the four gated accessibility events on the component it renders, which
|
|
537
|
+
// Angular forces to be unconditional and which would light that component's gates on every
|
|
538
|
+
// instance. It answers for them instead — see `gate-demand.ts`.
|
|
539
|
+
private readonly gateDemandAbove = injectGateDemandAbove();
|
|
540
|
+
|
|
541
|
+
wantsGate(name: IGatedAccessibilityEvent): boolean {
|
|
542
|
+
return gateWanted(this.gateDemandAbove, name, this[name]);
|
|
543
|
+
}
|
|
544
|
+
|
|
514
545
|
// RN's onShowUnderlay / onHideUnderlay — fired on a real transition only, never on a repeat.
|
|
515
546
|
@Output() readonly showUnderlay = new EventEmitter<void>();
|
|
516
547
|
@Output() readonly hideUnderlay = new EventEmitter<void>();
|
|
@@ -693,6 +724,7 @@ export class TouchableHighlight
|
|
|
693
724
|
@Component({
|
|
694
725
|
selector: 'TouchableWithoutFeedback',
|
|
695
726
|
standalone: true,
|
|
727
|
+
viewProviders: [provideGateDemand(() => TouchableWithoutFeedback)],
|
|
696
728
|
hostDirectives: [
|
|
697
729
|
{ directive: SymbioteStyleInputDirective, inputs: ['style'] },
|
|
698
730
|
],
|
|
@@ -700,6 +732,7 @@ export class TouchableHighlight
|
|
|
700
732
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
701
733
|
template: `
|
|
702
734
|
<Pressable
|
|
735
|
+
[__minPressDuration]="0"
|
|
703
736
|
[style]="mergedStyle"
|
|
704
737
|
(press)="press.emit($event)"
|
|
705
738
|
(pressIn)="handlePressIn($event)"
|
|
@@ -791,6 +824,16 @@ export class TouchableWithoutFeedback
|
|
|
791
824
|
@Output() readonly accessibilityTap = new EventEmitter<ISymbioteEvent>();
|
|
792
825
|
@Output() readonly magicTap = new EventEmitter<ISymbioteEvent>();
|
|
793
826
|
@Output() readonly accessibilityEscape = new EventEmitter<ISymbioteEvent>();
|
|
827
|
+
|
|
828
|
+
// This wrapper binds the four gated accessibility events on the component it renders, which
|
|
829
|
+
// Angular forces to be unconditional and which would light that component's gates on every
|
|
830
|
+
// instance. It answers for them instead — see `gate-demand.ts`.
|
|
831
|
+
private readonly gateDemandAbove = injectGateDemandAbove();
|
|
832
|
+
|
|
833
|
+
wantsGate(name: IGatedAccessibilityEvent): boolean {
|
|
834
|
+
return gateWanted(this.gateDemandAbove, name, this[name]);
|
|
835
|
+
}
|
|
836
|
+
|
|
794
837
|
@Input() delayPressIn?: number;
|
|
795
838
|
@Input() delayPressOut?: number;
|
|
796
839
|
@Input() minPressDuration?: number;
|
|
@@ -82,6 +82,7 @@ export type IAngularTouchableNativeFeedbackProps = Omit<
|
|
|
82
82
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
83
83
|
template: `
|
|
84
84
|
<Pressable
|
|
85
|
+
[__minPressDuration]="0"
|
|
85
86
|
(press)="press.emit($event)"
|
|
86
87
|
(pressIn)="pressIn.emit($event)"
|
|
87
88
|
(pressOut)="pressOut.emit($event)"
|
|
@@ -100,13 +101,7 @@ export type IAngularTouchableNativeFeedbackProps = Omit<
|
|
|
100
101
|
[android_ripple]="android_ripple"
|
|
101
102
|
[android_disableSound]="android_disableSound"
|
|
102
103
|
>
|
|
103
|
-
<symbiote-view
|
|
104
|
-
[symbioteHostProps]="hostProps()"
|
|
105
|
-
(accessibilityAction)="emit(accessibilityAction, $event)"
|
|
106
|
-
(accessibilityTap)="emit(accessibilityTap, $event)"
|
|
107
|
-
(magicTap)="emit(magicTap, $event)"
|
|
108
|
-
(accessibilityEscape)="emit(accessibilityEscape, $event)"
|
|
109
|
-
>
|
|
104
|
+
<symbiote-view [symbioteHostProps]="hostProps()">
|
|
110
105
|
<ng-content></ng-content>
|
|
111
106
|
</symbiote-view>
|
|
112
107
|
</Pressable>
|
|
@@ -262,14 +257,29 @@ export class TouchableNativeFeedback
|
|
|
262
257
|
nativeID: this.nativeID,
|
|
263
258
|
accessible: this.accessible,
|
|
264
259
|
...this.folded,
|
|
260
|
+
onAccessibilityAction: this.eventEmitterHandler(this.accessibilityAction),
|
|
261
|
+
onAccessibilityTap: this.eventEmitterHandler(this.accessibilityTap),
|
|
262
|
+
onMagicTap: this.eventEmitterHandler(this.magicTap),
|
|
263
|
+
onAccessibilityEscape: this.eventEmitterHandler(this.accessibilityEscape),
|
|
265
264
|
};
|
|
266
265
|
});
|
|
267
266
|
|
|
268
267
|
// Forward an engine event to the matching @Output(), narrowing the template's untyped $event.
|
|
269
|
-
emit(emitter: EventEmitter<ISymbioteEvent>, event: unknown): void {
|
|
268
|
+
private emit(emitter: EventEmitter<ISymbioteEvent>, event: unknown): void {
|
|
270
269
|
if (isSymbioteEvent(event)) emitter.emit(event);
|
|
271
270
|
}
|
|
272
271
|
|
|
272
|
+
// The four accessibility events are boolean-GATED Fabric events
|
|
273
|
+
// (`.claude/rules/fabric-boolean-event-gates.md`): native fires them only when the committed
|
|
274
|
+
// payload carries a FUNCTION at that key, so a template binding here lit the gate on every
|
|
275
|
+
// instance whether or not an app ever subscribed. `.observed`-gated, mirroring Pressable's
|
|
276
|
+
// `accessibilityEmitterHandler`.
|
|
277
|
+
private eventEmitterHandler(
|
|
278
|
+
emitter: EventEmitter<ISymbioteEvent>,
|
|
279
|
+
): ((event: unknown) => void) | undefined {
|
|
280
|
+
return emitter.observed ? event => this.emit(emitter, event) : undefined;
|
|
281
|
+
}
|
|
282
|
+
|
|
273
283
|
// Fold the web aria-*/role aliases into the canonical accessibility* props once per render, so the
|
|
274
284
|
// host never sees an aria-* key (native ignores them), and merge `disabled` into the a11y state
|
|
275
285
|
// (resolveDisabledAccessibilityState) the same way Pressable does for its own host.
|
|
@@ -127,6 +127,13 @@ export {
|
|
|
127
127
|
VListItemDirective,
|
|
128
128
|
VListSeparatorDirective,
|
|
129
129
|
} from './directives';
|
|
130
|
+
import {
|
|
131
|
+
gateWanted,
|
|
132
|
+
injectGateDemandAbove,
|
|
133
|
+
provideGateDemand,
|
|
134
|
+
type IGateDemand,
|
|
135
|
+
type IGatedAccessibilityEvent,
|
|
136
|
+
} from '../../gate-demand';
|
|
130
137
|
export type { IVListItemContext, IVListSeparatorContext } from './directives';
|
|
131
138
|
|
|
132
139
|
// The Angular VirtualizedList prop surface. Mirrors React/Vue's IVirtualizedListProps MINUS the
|
|
@@ -213,6 +220,7 @@ interface IWindowCell<ItemT> {
|
|
|
213
220
|
@Component({
|
|
214
221
|
selector: 'VirtualizedList',
|
|
215
222
|
standalone: true,
|
|
223
|
+
viewProviders: [provideGateDemand(() => VirtualizedList)],
|
|
216
224
|
hostDirectives: [
|
|
217
225
|
{ directive: SymbioteStyleInputDirective, inputs: ['style'] },
|
|
218
226
|
],
|
|
@@ -381,6 +389,16 @@ export class VirtualizedList<ItemT = unknown>
|
|
|
381
389
|
@Output() readonly accessibilityTap = new EventEmitter<ISymbioteEvent>();
|
|
382
390
|
@Output() readonly magicTap = new EventEmitter<ISymbioteEvent>();
|
|
383
391
|
@Output() readonly accessibilityEscape = new EventEmitter<ISymbioteEvent>();
|
|
392
|
+
|
|
393
|
+
// This wrapper binds the four gated accessibility events on the component it renders, which
|
|
394
|
+
// Angular forces to be unconditional and which would light that component's gates on every
|
|
395
|
+
// instance. It answers for them instead — see `gate-demand.ts`.
|
|
396
|
+
private readonly gateDemandAbove = injectGateDemandAbove();
|
|
397
|
+
|
|
398
|
+
wantsGate(name: IGatedAccessibilityEvent): boolean {
|
|
399
|
+
return gateWanted(this.gateDemandAbove, name, this[name]);
|
|
400
|
+
}
|
|
401
|
+
|
|
384
402
|
@Input({ required: true }) data!: unknown;
|
|
385
403
|
@Input({ required: true }) getItem!: (data: unknown, index: number) => ItemT;
|
|
386
404
|
@Input({ required: true }) getItemCount!: (data: unknown) => number;
|
|
@@ -88,6 +88,13 @@ export {
|
|
|
88
88
|
VSectionItemDirective,
|
|
89
89
|
VSectionSeparatorDirective,
|
|
90
90
|
} from './directives';
|
|
91
|
+
import {
|
|
92
|
+
gateWanted,
|
|
93
|
+
injectGateDemandAbove,
|
|
94
|
+
provideGateDemand,
|
|
95
|
+
type IGateDemand,
|
|
96
|
+
type IGatedAccessibilityEvent,
|
|
97
|
+
} from '../../gate-demand';
|
|
91
98
|
export type { IVSectionContext, IVSectionItemContext } from './directives';
|
|
92
99
|
|
|
93
100
|
// The Angular VirtualizedSectionList prop surface. Mirrors React/Vue's IVirtualizedSectionListProps
|
|
@@ -160,6 +167,7 @@ export type IVirtualizedSectionListInputs<ItemT> = Omit<
|
|
|
160
167
|
@Component({
|
|
161
168
|
selector: 'VirtualizedSectionList',
|
|
162
169
|
standalone: true,
|
|
170
|
+
viewProviders: [provideGateDemand(() => VirtualizedSectionList)],
|
|
163
171
|
hostDirectives: [
|
|
164
172
|
{ directive: SymbioteStyleInputDirective, inputs: ['style'] },
|
|
165
173
|
],
|
|
@@ -416,6 +424,16 @@ export class VirtualizedSectionList<ItemT = unknown>
|
|
|
416
424
|
@Output() readonly accessibilityTap = new EventEmitter<ISymbioteEvent>();
|
|
417
425
|
@Output() readonly magicTap = new EventEmitter<ISymbioteEvent>();
|
|
418
426
|
@Output() readonly accessibilityEscape = new EventEmitter<ISymbioteEvent>();
|
|
427
|
+
|
|
428
|
+
// This wrapper binds the four gated accessibility events on the component it renders, which
|
|
429
|
+
// Angular forces to be unconditional and which would light that component's gates on every
|
|
430
|
+
// instance. It answers for them instead — see `gate-demand.ts`.
|
|
431
|
+
private readonly gateDemandAbove = injectGateDemandAbove();
|
|
432
|
+
|
|
433
|
+
wantsGate(name: IGatedAccessibilityEvent): boolean {
|
|
434
|
+
return gateWanted(this.gateDemandAbove, name, this[name]);
|
|
435
|
+
}
|
|
436
|
+
|
|
419
437
|
@Input() role?: IAriaProps['role'];
|
|
420
438
|
@Input() ariaLabel?: string;
|
|
421
439
|
@Input() ariaLabelledBy?: string;
|
package/src/components.ts
CHANGED
|
@@ -20,16 +20,23 @@
|
|
|
20
20
|
//
|
|
21
21
|
// The safe, fully-public-API mechanism: create the embedded view DIRECTLY inside a
|
|
22
22
|
// ViewContainerRef anchored at the destination, so there is nothing to move at all. That
|
|
23
|
-
// ViewContainerRef comes from `PortalOutletDirective`, a
|
|
24
|
-
//
|
|
25
|
-
//
|
|
26
|
-
// `
|
|
27
|
-
//
|
|
28
|
-
//
|
|
29
|
-
//
|
|
23
|
+
// ViewContainerRef comes from `PortalOutletDirective`, exported to a template variable with the
|
|
24
|
+
// same idiom `#form="ngForm"` uses. This also replaces the `isSymbioteNode` runtime guard
|
|
25
|
+
// React/Vue need (there `to` is an arbitrary JS value until validated): here `to` is typed as
|
|
26
|
+
// `PortalOutletDirective` — the only way to construct one is Angular's own template compiler
|
|
27
|
+
// resolving a template reference variable, so `strictTemplates` rejects anything else at compile
|
|
28
|
+
// time and there is nothing left to guard at runtime.
|
|
29
|
+
//
|
|
30
|
+
// THE MARKER GOES ON AN `<ng-container>` INSIDE THE TARGET, never on the target element. A
|
|
31
|
+
// ViewContainerRef anchors AT its host and `createEmbeddedView` inserts after that anchor, so
|
|
32
|
+
// `<View portalOutlet>` delivers a SIBLING where React's `createPortal(node, host)` and Vue's
|
|
33
|
+
// `<Teleport to>` deliver a child. Device-reported 2026-09-02: a card portaled into an absolutely
|
|
34
|
+
// positioned overlay laid out in the scroll flow, since the overlay cannot centre a node that is
|
|
35
|
+
// not in it.
|
|
30
36
|
|
|
31
37
|
import {
|
|
32
38
|
Directive,
|
|
39
|
+
ElementRef,
|
|
33
40
|
inject,
|
|
34
41
|
Input,
|
|
35
42
|
TemplateRef,
|
|
@@ -38,10 +45,19 @@ import {
|
|
|
38
45
|
type OnChanges,
|
|
39
46
|
type OnDestroy,
|
|
40
47
|
} from '@angular/core';
|
|
48
|
+
import { isAnchor, isSymbioteNode } from '@symbiote-native/engine';
|
|
41
49
|
|
|
42
50
|
/** Marks the destination for `*portal` — place it on whichever already-mounted host should
|
|
43
|
-
* paint the portaled content
|
|
44
|
-
* export it to a template variable and pass that
|
|
51
|
+
* paint the portaled content — on an `<ng-container>` INSIDE that host, never on the host
|
|
52
|
+
* element itself (see the file header) — then export it to a template variable and pass that
|
|
53
|
+
* variable as `*portal`'s target:
|
|
54
|
+
*
|
|
55
|
+
* ```html
|
|
56
|
+
* <View class="overlay-host">
|
|
57
|
+
* <ng-container portalOutlet #overlayHost="portalOutlet"></ng-container>
|
|
58
|
+
* </View>
|
|
59
|
+
* ```
|
|
60
|
+
*/
|
|
45
61
|
@Directive({
|
|
46
62
|
selector: '[portalOutlet]',
|
|
47
63
|
standalone: true,
|
|
@@ -49,6 +65,20 @@ import {
|
|
|
49
65
|
})
|
|
50
66
|
export class PortalOutletDirective {
|
|
51
67
|
readonly viewContainerRef = inject(ViewContainerRef);
|
|
68
|
+
|
|
69
|
+
constructor() {
|
|
70
|
+
// The wrong placement commits a valid tree that simply differs from React's, so nothing
|
|
71
|
+
// downstream can catch it. It has to fail where it is written.
|
|
72
|
+
const host: unknown = inject(ElementRef).nativeElement;
|
|
73
|
+
if (!isSymbioteNode(host) || !isAnchor(host)) {
|
|
74
|
+
throw new Error(
|
|
75
|
+
'portalOutlet must sit on an <ng-container> inside the target host, not on the host ' +
|
|
76
|
+
'element: a ViewContainerRef anchored at an element inserts AFTER it, so the ported ' +
|
|
77
|
+
'content would become the host\'s sibling. Write <View class="…">' +
|
|
78
|
+
'<ng-container portalOutlet #x="portalOutlet"></ng-container></View>.',
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
52
82
|
}
|
|
53
83
|
|
|
54
84
|
/** `*portal="overlayHost"` — renders the host element into whichever `PortalOutletDirective`
|