@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
|
@@ -1,4 +1,9 @@
|
|
|
1
1
|
import { EventEmitter, computed, signal } from '@angular/core';
|
|
2
|
+
import {
|
|
3
|
+
gateWanted,
|
|
4
|
+
injectGateDemand,
|
|
5
|
+
type IGatedAccessibilityEvent,
|
|
6
|
+
} from '../../gate-demand';
|
|
2
7
|
import {
|
|
3
8
|
imageStatics,
|
|
4
9
|
renderImage,
|
|
@@ -162,6 +167,14 @@ function asStyle(value: unknown): IStyleProp<IViewStyle> | undefined {
|
|
|
162
167
|
return typeof value === 'object' && value !== null ? value : undefined;
|
|
163
168
|
}
|
|
164
169
|
|
|
170
|
+
// Narrows a value out of the untyped animated prop bag. A runtime guard rather than a cast: the
|
|
171
|
+
// bag is built at runtime from merged animated values, so nothing upstream has proven the shape.
|
|
172
|
+
export function isImageEventCallback(
|
|
173
|
+
value: unknown,
|
|
174
|
+
): value is (event: ISymbioteEvent) => void {
|
|
175
|
+
return typeof value === 'function';
|
|
176
|
+
}
|
|
177
|
+
|
|
165
178
|
export function resolveImageProps(
|
|
166
179
|
input: Record<string, unknown>,
|
|
167
180
|
): Record<string, unknown> {
|
|
@@ -382,28 +395,32 @@ export abstract class ImageBase {
|
|
|
382
395
|
readonly progress = new EventEmitter<ISymbioteEvent>();
|
|
383
396
|
readonly partialLoad = new EventEmitter<ISymbioteEvent>();
|
|
384
397
|
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
this
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
if (!
|
|
405
|
-
|
|
406
|
-
|
|
398
|
+
// Null unless an adapter wrapper renders this Image in its own template — ImageBackground does.
|
|
399
|
+
// An app's own `<Image>` gets null and answers from its own `.observed`.
|
|
400
|
+
private readonly gateDemand = injectGateDemand();
|
|
401
|
+
|
|
402
|
+
// The four accessibility events are boolean-GATED Fabric events
|
|
403
|
+
// (`.claude/rules/fabric-boolean-event-gates.md`): native fires them only when the committed
|
|
404
|
+
// payload carries a FUNCTION at that key. A template `(accessibilityAction)="..."` binding on the
|
|
405
|
+
// intrinsic host lights the gate unconditionally; this reaches the app two ways — a plain
|
|
406
|
+
// `[onAccessibilityAction]` @Input callback, or the `accessibilityAction` @Output — and the gate
|
|
407
|
+
// must light only when at least one of the two is actually wired. Returned `undefined` means
|
|
408
|
+
// `setEventListener` never sees a function, so the gate stays dark.
|
|
409
|
+
protected gatedAccessibilityHandler(
|
|
410
|
+
name: IGatedAccessibilityEvent,
|
|
411
|
+
callback: ((event: ISymbioteEvent) => void) | undefined,
|
|
412
|
+
): ((event: Event) => void) | undefined {
|
|
413
|
+
const emitter = this[name];
|
|
414
|
+
// A WRAPPER's binding is not a subscriber, and `.observed` cannot tell the two apart.
|
|
415
|
+
// ImageBackground renders this component and binds all four, so without the demand every
|
|
416
|
+
// Image inside one lit its gates. See `gate-demand.ts`.
|
|
417
|
+
if (!gateWanted(this.gateDemand, name, emitter) && callback === undefined)
|
|
418
|
+
return undefined;
|
|
419
|
+
return event => {
|
|
420
|
+
if (!isSymbioteEvent(event)) return;
|
|
421
|
+
callback?.(event);
|
|
422
|
+
emitter.emit(event);
|
|
423
|
+
};
|
|
407
424
|
}
|
|
408
425
|
|
|
409
426
|
handleLoadStart(event: Event): void {
|
|
@@ -499,10 +516,19 @@ export abstract class ImageBase {
|
|
|
499
516
|
ariaValueMin: this.ariaValueMin,
|
|
500
517
|
ariaValueNow: this.ariaValueNow,
|
|
501
518
|
ariaValueText: this.ariaValueText,
|
|
502
|
-
onAccessibilityAction: this.
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
519
|
+
onAccessibilityAction: this.gatedAccessibilityHandler(
|
|
520
|
+
'accessibilityAction',
|
|
521
|
+
this.onAccessibilityAction,
|
|
522
|
+
),
|
|
523
|
+
onAccessibilityTap: this.gatedAccessibilityHandler(
|
|
524
|
+
'accessibilityTap',
|
|
525
|
+
this.onAccessibilityTap,
|
|
526
|
+
),
|
|
527
|
+
onMagicTap: this.gatedAccessibilityHandler('magicTap', this.onMagicTap),
|
|
528
|
+
onAccessibilityEscape: this.gatedAccessibilityHandler(
|
|
529
|
+
'accessibilityEscape',
|
|
530
|
+
this.onAccessibilityEscape,
|
|
531
|
+
),
|
|
506
532
|
onLoadStart: this.onLoadStart,
|
|
507
533
|
onLoad: this.onLoad,
|
|
508
534
|
onLoadEnd: this.onLoadEnd,
|
|
@@ -42,6 +42,13 @@ import {
|
|
|
42
42
|
SymbioteStyleInputDirective,
|
|
43
43
|
ViewHost,
|
|
44
44
|
} from '../primitives';
|
|
45
|
+
import {
|
|
46
|
+
gateWanted,
|
|
47
|
+
injectGateDemandAbove,
|
|
48
|
+
provideGateDemand,
|
|
49
|
+
type IGateDemand,
|
|
50
|
+
type IGatedAccessibilityEvent,
|
|
51
|
+
} from '../gate-demand';
|
|
45
52
|
|
|
46
53
|
// Mirrors React's IImageBackgroundProps minus children (Angular takes children via <ng-content>):
|
|
47
54
|
// every forwarding Image prop flows onto the inner image; `style` is the WRAPPER View style and
|
|
@@ -75,6 +82,7 @@ export type IAngularImageBackgroundInputs = Omit<
|
|
|
75
82
|
@Component({
|
|
76
83
|
selector: 'ImageBackground',
|
|
77
84
|
standalone: true,
|
|
85
|
+
viewProviders: [provideGateDemand(() => ImageBackground)],
|
|
78
86
|
hostDirectives: [
|
|
79
87
|
{ directive: SymbioteStyleInputDirective, inputs: ['style'] },
|
|
80
88
|
],
|
|
@@ -150,6 +158,16 @@ export class ImageBackground implements IAngularImageBackgroundInputs {
|
|
|
150
158
|
@Output() readonly accessibilityTap = new EventEmitter<ISymbioteEvent>();
|
|
151
159
|
@Output() readonly magicTap = new EventEmitter<ISymbioteEvent>();
|
|
152
160
|
@Output() readonly accessibilityEscape = new EventEmitter<ISymbioteEvent>();
|
|
161
|
+
|
|
162
|
+
// This wrapper binds the four gated accessibility events on the component it renders, which
|
|
163
|
+
// Angular forces to be unconditional and which would light that component's gates on every
|
|
164
|
+
// instance. It answers for them instead — see `gate-demand.ts`.
|
|
165
|
+
private readonly gateDemandAbove = injectGateDemandAbove();
|
|
166
|
+
|
|
167
|
+
wantsGate(name: IGatedAccessibilityEvent): boolean {
|
|
168
|
+
return gateWanted(this.gateDemandAbove, name, this[name]);
|
|
169
|
+
}
|
|
170
|
+
|
|
153
171
|
@Output() readonly loadStart = new EventEmitter<ISymbioteEvent>();
|
|
154
172
|
@Output() readonly load = new EventEmitter<ISymbioteEvent>();
|
|
155
173
|
@Output() readonly loadEnd = new EventEmitter<ISymbioteEvent>();
|
|
@@ -72,13 +72,7 @@ export type IAngularInputAccessoryViewInputs = Omit<
|
|
|
72
72
|
imports: [InputAccessoryViewHost, SymbioteHostPropsDirective],
|
|
73
73
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
74
74
|
template: `
|
|
75
|
-
<symbiote-input-accessory-view
|
|
76
|
-
[symbioteHostProps]="hostProps()"
|
|
77
|
-
(accessibilityAction)="emit(accessibilityAction, $event)"
|
|
78
|
-
(accessibilityTap)="emit(accessibilityTap, $event)"
|
|
79
|
-
(magicTap)="emit(magicTap, $event)"
|
|
80
|
-
(accessibilityEscape)="emit(accessibilityEscape, $event)"
|
|
81
|
-
>
|
|
75
|
+
<symbiote-input-accessory-view [symbioteHostProps]="hostProps()">
|
|
82
76
|
<ng-content></ng-content>
|
|
83
77
|
</symbiote-input-accessory-view>
|
|
84
78
|
`,
|
|
@@ -136,10 +130,18 @@ export class InputAccessoryView
|
|
|
136
130
|
private readonly elementRef = inject(ElementRef);
|
|
137
131
|
|
|
138
132
|
// Forward an engine event to the matching @Output(), narrowing the template's untyped $event.
|
|
139
|
-
emit(emitter: EventEmitter<ISymbioteEvent>, event: unknown): void {
|
|
133
|
+
private emit(emitter: EventEmitter<ISymbioteEvent>, event: unknown): void {
|
|
140
134
|
if (isSymbioteEvent(event)) emitter.emit(event);
|
|
141
135
|
}
|
|
142
136
|
|
|
137
|
+
// The four accessibility events are boolean-GATED Fabric events
|
|
138
|
+
// (`.claude/rules/fabric-boolean-event-gates.md`). `.observed`-gated, mirroring Pressable's.
|
|
139
|
+
private eventEmitterHandler(
|
|
140
|
+
emitter: EventEmitter<ISymbioteEvent>,
|
|
141
|
+
): ((event: unknown) => void) | undefined {
|
|
142
|
+
return emitter.observed ? event => this.emit(emitter, event) : undefined;
|
|
143
|
+
}
|
|
144
|
+
|
|
143
145
|
// renderInputAccessoryView owns the host-node assembly (shared with React/Vue); the adapter reads
|
|
144
146
|
// the resolved host props off the Descriptor it returns.
|
|
145
147
|
private get descriptor() {
|
|
@@ -190,6 +192,10 @@ export class InputAccessoryView
|
|
|
190
192
|
return {
|
|
191
193
|
...descriptorProps,
|
|
192
194
|
style: [anchorHostStyle(this.elementRef), descriptorProps.style],
|
|
195
|
+
onAccessibilityAction: this.eventEmitterHandler(this.accessibilityAction),
|
|
196
|
+
onAccessibilityTap: this.eventEmitterHandler(this.accessibilityTap),
|
|
197
|
+
onMagicTap: this.eventEmitterHandler(this.magicTap),
|
|
198
|
+
onAccessibilityEscape: this.eventEmitterHandler(this.accessibilityEscape),
|
|
193
199
|
};
|
|
194
200
|
});
|
|
195
201
|
|
|
@@ -96,13 +96,15 @@ export type IAngularKeyboardAvoidingViewInputs = Omit<
|
|
|
96
96
|
// The wrapper carries a11y + onLayout; 'position' ('nested') pushes the children in an inner
|
|
97
97
|
// View by `bottom: inset`, the wrapper modes adjust the single wrapper directly. Only one @if
|
|
98
98
|
// branch is instantiated, so each branch's <ng-content> projects the children unambiguously.
|
|
99
|
+
// `layout` stays a plain, unconditional template binding: unlike the four accessibility events
|
|
100
|
+
// below, this component READS its own onLayout internally (handleLayout measures the frame that
|
|
101
|
+
// feeds the next keyboard event's inset math — see keyboard-avoiding-view-rn-contract.md), so the
|
|
102
|
+
// gate must stay lit regardless of whether the app subscribes to the `layout` @Output(). Gating it
|
|
103
|
+
// on `.observed` would silently break the inset fixpoint correction the moment an app didn't
|
|
104
|
+
// listen to `layout`.
|
|
99
105
|
template: `
|
|
100
106
|
<symbiote-view
|
|
101
107
|
[symbioteHostProps]="hostProps"
|
|
102
|
-
(accessibilityAction)="emit(accessibilityAction, $event)"
|
|
103
|
-
(accessibilityTap)="emit(accessibilityTap, $event)"
|
|
104
|
-
(magicTap)="emit(magicTap, $event)"
|
|
105
|
-
(accessibilityEscape)="emit(accessibilityEscape, $event)"
|
|
106
108
|
(layout)="handleLayout($event)"
|
|
107
109
|
>
|
|
108
110
|
@if (isNested) {
|
|
@@ -296,16 +298,28 @@ export class KeyboardAvoidingView
|
|
|
296
298
|
nativeID: this.nativeID,
|
|
297
299
|
accessible: this.accessible,
|
|
298
300
|
...this.folded,
|
|
301
|
+
onAccessibilityAction: this.eventEmitterHandler(this.accessibilityAction),
|
|
302
|
+
onAccessibilityTap: this.eventEmitterHandler(this.accessibilityTap),
|
|
303
|
+
onMagicTap: this.eventEmitterHandler(this.magicTap),
|
|
304
|
+
onAccessibilityEscape: this.eventEmitterHandler(this.accessibilityEscape),
|
|
299
305
|
};
|
|
300
306
|
}
|
|
301
307
|
|
|
302
308
|
// Forward an engine event to the matching @Output(), narrowing the template's untyped $event
|
|
303
|
-
// first.
|
|
304
|
-
|
|
305
|
-
emit(emitter: EventEmitter<ISymbioteEvent>, event: unknown): void {
|
|
309
|
+
// first.
|
|
310
|
+
private emit(emitter: EventEmitter<ISymbioteEvent>, event: unknown): void {
|
|
306
311
|
if (isSymbioteEvent(event)) emitter.emit(event);
|
|
307
312
|
}
|
|
308
313
|
|
|
314
|
+
// The four accessibility events are boolean-GATED Fabric events
|
|
315
|
+
// (`.claude/rules/fabric-boolean-event-gates.md`). `.observed`-gated, unlike `layout` above which
|
|
316
|
+
// this component needs unconditionally for its own internal frame measurement.
|
|
317
|
+
private eventEmitterHandler(
|
|
318
|
+
emitter: EventEmitter<ISymbioteEvent>,
|
|
319
|
+
): ((event: unknown) => void) | undefined {
|
|
320
|
+
return emitter.observed ? event => this.emit(emitter, event) : undefined;
|
|
321
|
+
}
|
|
322
|
+
|
|
309
323
|
// Fold the web aria-*/role aliases into the canonical accessibility* props once per render, so the
|
|
310
324
|
// host node never sees an aria-* key (native ignores them) — the shared transform every adapter runs.
|
|
311
325
|
get folded(): Partial<IAngularKeyboardAvoidingViewProps> {
|
|
@@ -115,10 +115,6 @@ export type IAngularModalInputs = Omit<
|
|
|
115
115
|
(dismiss)="dismiss.emit()"
|
|
116
116
|
(requestClose)="requestClose.emit()"
|
|
117
117
|
(orientationChange)="emit(orientationChange, $event)"
|
|
118
|
-
(accessibilityAction)="emit(accessibilityAction, $event)"
|
|
119
|
-
(accessibilityTap)="emit(accessibilityTap, $event)"
|
|
120
|
-
(magicTap)="emit(magicTap, $event)"
|
|
121
|
-
(accessibilityEscape)="emit(accessibilityEscape, $event)"
|
|
122
118
|
>
|
|
123
119
|
<symbiote-view [style]="containerStyle" [collapsable]="false">
|
|
124
120
|
<ng-content></ng-content>
|
|
@@ -279,6 +275,10 @@ export class Modal implements IAngularModalInputs, OnInit, OnChanges, DoCheck {
|
|
|
279
275
|
return {
|
|
280
276
|
...descriptorProps,
|
|
281
277
|
style: [anchorHostStyle(this.elementRef), descriptorProps.style],
|
|
278
|
+
onAccessibilityAction: this.eventEmitterHandler(this.accessibilityAction),
|
|
279
|
+
onAccessibilityTap: this.eventEmitterHandler(this.accessibilityTap),
|
|
280
|
+
onMagicTap: this.eventEmitterHandler(this.magicTap),
|
|
281
|
+
onAccessibilityEscape: this.eventEmitterHandler(this.accessibilityEscape),
|
|
282
282
|
};
|
|
283
283
|
});
|
|
284
284
|
|
|
@@ -287,10 +287,23 @@ export class Modal implements IAngularModalInputs, OnInit, OnChanges, DoCheck {
|
|
|
287
287
|
return typeof container === 'string' ? undefined : container.props.style;
|
|
288
288
|
}
|
|
289
289
|
|
|
290
|
+
// NOT private: `(orientationChange)="emit(...)"` in the template above calls it, and a template
|
|
291
|
+
// can only reach public members. `tsc` does not typecheck Angular templates, so `private` here
|
|
292
|
+
// compiles clean and fails only under `ngc --compilationMode partial`.
|
|
290
293
|
emit(emitter: EventEmitter<ISymbioteEvent>, event: unknown): void {
|
|
291
294
|
if (isSymbioteEvent(event)) emitter.emit(event);
|
|
292
295
|
}
|
|
293
296
|
|
|
297
|
+
// The four accessibility events are boolean-GATED Fabric events
|
|
298
|
+
// (`.claude/rules/fabric-boolean-event-gates.md`) — `show`/`dismiss`/`requestClose`/
|
|
299
|
+
// `orientationChange` are NOT (Modal's own MODAL_EVENTS), so those stay unconditional template
|
|
300
|
+
// bindings above. `.observed`-gated, mirroring Pressable's.
|
|
301
|
+
private eventEmitterHandler(
|
|
302
|
+
emitter: EventEmitter<ISymbioteEvent>,
|
|
303
|
+
): ((event: unknown) => void) | undefined {
|
|
304
|
+
return emitter.observed ? event => this.emit(emitter, event) : undefined;
|
|
305
|
+
}
|
|
306
|
+
|
|
294
307
|
// Typed as the a11y intersection WITH the string index (the bag renderModal spreads into the
|
|
295
308
|
// host props), so resolveAccessibilityProps's result stays assignable to passthrough.
|
|
296
309
|
private accessibilityInputs(): IAccessibilityProps &
|
|
@@ -13,11 +13,14 @@ import {
|
|
|
13
13
|
ViewChild,
|
|
14
14
|
type DoCheck,
|
|
15
15
|
type OnChanges,
|
|
16
|
+
type OnDestroy,
|
|
16
17
|
} from '@angular/core';
|
|
17
18
|
import {
|
|
18
19
|
createPressHandlers,
|
|
19
20
|
createPressRuntime,
|
|
21
|
+
disposePressRuntime,
|
|
20
22
|
DEFAULT_DELAY_LONG_PRESS_MS,
|
|
23
|
+
DEFAULT_MIN_PRESS_DURATION_MS,
|
|
21
24
|
isTerminationAllowed,
|
|
22
25
|
resolveAccessibilityProps,
|
|
23
26
|
resolveDisabledAccessibilityState,
|
|
@@ -50,6 +53,11 @@ import {
|
|
|
50
53
|
SymbioteStyleInputDirective,
|
|
51
54
|
ViewHost,
|
|
52
55
|
} from '../../primitives';
|
|
56
|
+
import {
|
|
57
|
+
gateWanted,
|
|
58
|
+
injectGateDemand,
|
|
59
|
+
type IGatedAccessibilityEvent,
|
|
60
|
+
} from '../../gate-demand';
|
|
53
61
|
|
|
54
62
|
export type {
|
|
55
63
|
IPressState,
|
|
@@ -123,7 +131,18 @@ function asSymbioteEvent(event: unknown): ISymbioteEvent | undefined {
|
|
|
123
131
|
}
|
|
124
132
|
|
|
125
133
|
@Component({
|
|
126
|
-
selector
|
|
134
|
+
// Single-name selector, matching TextInput's own ('TextInput', text-input.ts) rather than
|
|
135
|
+
// View/Text's dual one. The alternate `symbiote-pressable` spelling was vestigial — nothing in
|
|
136
|
+
// this component's own template ever renders that tag (it renders `symbiote-view`, below), and
|
|
137
|
+
// no app or example code invokes the composed component by that literal tag (verified by grep,
|
|
138
|
+
// 2026-08-31). Its only effect was to force `symbiote-pressable` into
|
|
139
|
+
// `ANCHOR_HOST_COMPONENTS` unconditionally, which collided with the tag's OTHER, load-bearing
|
|
140
|
+
// meaning — the host-behavior registry key (`PRESSABLE_TAG`,
|
|
141
|
+
// core/components/src/behaviors/pressable.ts) a lowered `<Pressable>` must carry to get the
|
|
142
|
+
// press machine. That collision is exactly why `DIAGNOSTIC_LOWERED_PRESSABLE_TAG` existed
|
|
143
|
+
// (renderer/index.ts) — dropping the vestigial spelling here removes the collision at its
|
|
144
|
+
// source, so the escape hatch is retired rather than worked around.
|
|
145
|
+
selector: 'Pressable',
|
|
127
146
|
standalone: true,
|
|
128
147
|
hostDirectives: [
|
|
129
148
|
{ directive: SymbioteStyleInputDirective, inputs: ['style'] },
|
|
@@ -134,10 +153,6 @@ function asSymbioteEvent(event: unknown): ISymbioteEvent | undefined {
|
|
|
134
153
|
<symbiote-view
|
|
135
154
|
#host
|
|
136
155
|
[symbioteHostProps]="hostProps()"
|
|
137
|
-
(accessibilityAction)="emit(accessibilityAction, $event)"
|
|
138
|
-
(accessibilityTap)="emit(accessibilityTap, $event)"
|
|
139
|
-
(magicTap)="emit(magicTap, $event)"
|
|
140
|
-
(accessibilityEscape)="emit(accessibilityEscape, $event)"
|
|
141
156
|
(press)="handlePress($event)"
|
|
142
157
|
(pressIn)="handlePressIn($event)"
|
|
143
158
|
(pressOut)="handlePressOut($event)"
|
|
@@ -150,7 +165,9 @@ function asSymbioteEvent(event: unknown): ISymbioteEvent | undefined {
|
|
|
150
165
|
`,
|
|
151
166
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
152
167
|
})
|
|
153
|
-
export class Pressable
|
|
168
|
+
export class Pressable
|
|
169
|
+
implements IAngularPressableInputs, OnChanges, OnDestroy, DoCheck
|
|
170
|
+
{
|
|
154
171
|
// The press/hover lifecycle as real Angular events: `(press)="onTap($event)"`, not
|
|
155
172
|
// `[onPress]="onTap"`. createPressHandlers still wants plain IPressHandler callbacks, so
|
|
156
173
|
// emitterHandler() below adapts each EventEmitter into one - only while something is actually
|
|
@@ -163,9 +180,15 @@ export class Pressable implements IAngularPressableInputs, OnChanges, DoCheck {
|
|
|
163
180
|
@Output() readonly longPress = new EventEmitter<ISymbioteEvent>();
|
|
164
181
|
@Output() readonly hoverIn = new EventEmitter<ISymbioteEvent>();
|
|
165
182
|
@Output() readonly hoverOut = new EventEmitter<ISymbioteEvent>();
|
|
166
|
-
// The four accessibility callbacks as real Angular events
|
|
167
|
-
//
|
|
168
|
-
// `
|
|
183
|
+
// The four accessibility callbacks as real Angular events, app-facing. UNLIKE press/hover above,
|
|
184
|
+
// these are NOT bound in the template at all — `accessibilityAction`/`accessibilityTap`/
|
|
185
|
+
// `magicTap`/`accessibilityEscape` are `onX`-gated Fabric events
|
|
186
|
+
// (`.claude/rules/fabric-boolean-event-gates.md`): the engine only emits them when the committed
|
|
187
|
+
// payload carries a FUNCTION at that key, so a `(accessibilityAction)="..."` template binding —
|
|
188
|
+
// always present once written — would light the gate on every instance regardless of whether the
|
|
189
|
+
// app ever subscribes. Routed through `hostProps()` instead (`accessibilityEmitterHandler`
|
|
190
|
+
// below), the same `.observed`-conditional shape `emitterHandler` already uses for press/hover,
|
|
191
|
+
// so the gate lights only when something is actually listening.
|
|
169
192
|
@Output() readonly accessibilityAction = new EventEmitter<ISymbioteEvent>();
|
|
170
193
|
@Output() readonly accessibilityTap = new EventEmitter<ISymbioteEvent>();
|
|
171
194
|
@Output() readonly magicTap = new EventEmitter<ISymbioteEvent>();
|
|
@@ -176,6 +199,8 @@ export class Pressable implements IAngularPressableInputs, OnChanges, DoCheck {
|
|
|
176
199
|
@Input() hitSlop?: IRectOffset;
|
|
177
200
|
@Input() pressRetentionOffset?: IRectOffset;
|
|
178
201
|
@Input() unstable_pressDelay?: number;
|
|
202
|
+
/** @internal Touchable* mirrors RN's Pressability minPressDuration: 0 override. */
|
|
203
|
+
@Input() __minPressDuration = DEFAULT_MIN_PRESS_DURATION_MS;
|
|
179
204
|
@Input() android_ripple?: IPressableAndroidRippleConfig;
|
|
180
205
|
@Input() android_disableSound?: boolean;
|
|
181
206
|
@Input() delayHoverIn?: number;
|
|
@@ -255,9 +280,14 @@ export class Pressable implements IAngularPressableInputs, OnChanges, DoCheck {
|
|
|
255
280
|
const id = setTimeout(callback, ms);
|
|
256
281
|
return () => clearTimeout(id);
|
|
257
282
|
},
|
|
283
|
+
now: Date.now,
|
|
258
284
|
};
|
|
259
285
|
|
|
260
286
|
private readonly changeDetector = inject(ChangeDetectorRef);
|
|
287
|
+
// Null unless an adapter wrapper renders this Pressable in its own template. See
|
|
288
|
+
// `gate-demand.ts`; an app's own `<Pressable>` — including one projected INTO a wrapper — gets
|
|
289
|
+
// null and falls back to its own `.observed`.
|
|
290
|
+
private readonly gateDemand = injectGateDemand();
|
|
261
291
|
// This component's OWN host - the non-painting anchor `class="..."` at the use site resolves
|
|
262
292
|
// onto (see anchorHostStyle's doc comment) - NOT `hostElement` above, which targets the real
|
|
263
293
|
// inner `symbiote-view` one level down.
|
|
@@ -289,6 +319,10 @@ export class Pressable implements IAngularPressableInputs, OnChanges, DoCheck {
|
|
|
289
319
|
this.anchorStyle.set(anchorHostStyle(this.elementRef));
|
|
290
320
|
}
|
|
291
321
|
|
|
322
|
+
ngOnDestroy(): void {
|
|
323
|
+
disposePressRuntime(this.runtime);
|
|
324
|
+
}
|
|
325
|
+
|
|
292
326
|
ngOnChanges(): void {
|
|
293
327
|
// Tells the computed bag below that an @Input moved - plain class fields are not reactive on
|
|
294
328
|
// their own. Must stay in ngOnChanges: that is the single moment Angular has finished writing
|
|
@@ -371,6 +405,14 @@ export class Pressable implements IAngularPressableInputs, OnChanges, DoCheck {
|
|
|
371
405
|
style: [this.anchorStyle(), this.resolvedStyle],
|
|
372
406
|
accessible: this.accessible,
|
|
373
407
|
...this.foldedAccessibility,
|
|
408
|
+
onAccessibilityAction: this.accessibilityEmitterHandler(
|
|
409
|
+
'accessibilityAction',
|
|
410
|
+
),
|
|
411
|
+
onAccessibilityTap: this.accessibilityEmitterHandler('accessibilityTap'),
|
|
412
|
+
onMagicTap: this.accessibilityEmitterHandler('magicTap'),
|
|
413
|
+
onAccessibilityEscape: this.accessibilityEmitterHandler(
|
|
414
|
+
'accessibilityEscape',
|
|
415
|
+
),
|
|
374
416
|
android_disableSound: this.android_disableSound,
|
|
375
417
|
nativeBackgroundAndroid:
|
|
376
418
|
this.resolvedRippleProps?.nativeBackgroundAndroid,
|
|
@@ -415,7 +457,7 @@ export class Pressable implements IAngularPressableInputs, OnChanges, DoCheck {
|
|
|
415
457
|
return isTerminationAllowed(this.cancelable);
|
|
416
458
|
}
|
|
417
459
|
|
|
418
|
-
emit(emitter: EventEmitter<ISymbioteEvent>, event: unknown): void {
|
|
460
|
+
private emit(emitter: EventEmitter<ISymbioteEvent>, event: unknown): void {
|
|
419
461
|
const symbioteEvent = asSymbioteEvent(event);
|
|
420
462
|
if (symbioteEvent !== undefined) emitter.emit(symbioteEvent);
|
|
421
463
|
}
|
|
@@ -439,6 +481,23 @@ export class Pressable implements IAngularPressableInputs, OnChanges, DoCheck {
|
|
|
439
481
|
return emitter.observed ? event => emitter.emit(event) : undefined;
|
|
440
482
|
}
|
|
441
483
|
|
|
484
|
+
// The accessibility twin of emitterHandler, feeding hostProps()'s flat bag instead of
|
|
485
|
+
// createPressHandlers' config — see the accessibilityAction @Output()'s own comment for why
|
|
486
|
+
// these four specifically cannot be plain template `(x)="..."` bindings. `undefined` here means
|
|
487
|
+
// `setEventListener` never sees a function, so the gated prop's flag never lights
|
|
488
|
+
// (`fabric-boolean-event-gates.md`, "The flag follows the VALUE, not the key").
|
|
489
|
+
private accessibilityEmitterHandler(
|
|
490
|
+
name: IGatedAccessibilityEvent,
|
|
491
|
+
): ((event: unknown) => void) | undefined {
|
|
492
|
+
const emitter = this[name];
|
|
493
|
+
// A WRAPPER's binding is not a subscriber, and `.observed` cannot tell the two apart. When a
|
|
494
|
+
// demand is supplied from above it decides instead — see `gate-demand.ts` for why the channel
|
|
495
|
+
// is DI rather than an `@Input`.
|
|
496
|
+
return gateWanted(this.gateDemand, name, emitter)
|
|
497
|
+
? event => this.emit(emitter, event)
|
|
498
|
+
: undefined;
|
|
499
|
+
}
|
|
500
|
+
|
|
442
501
|
private get handlers() {
|
|
443
502
|
this.noteHoverNoop();
|
|
444
503
|
const config: IPressMachineConfig = {
|
|
@@ -449,6 +508,7 @@ export class Pressable implements IAngularPressableInputs, OnChanges, DoCheck {
|
|
|
449
508
|
onLongPress: this.emitterHandler(this.longPress),
|
|
450
509
|
delayLongPress: this.delayLongPress ?? DEFAULT_DELAY_LONG_PRESS_MS,
|
|
451
510
|
unstable_pressDelay: this.unstable_pressDelay ?? 0,
|
|
511
|
+
minPressDuration: this.__minPressDuration,
|
|
452
512
|
hitSlop: this.hitSlop,
|
|
453
513
|
pressRetentionOffset: this.pressRetentionOffset,
|
|
454
514
|
};
|
|
@@ -109,10 +109,6 @@ export type IAngularRefreshControlInputs = Omit<
|
|
|
109
109
|
#host
|
|
110
110
|
[symbioteHostProps]="hostProps()"
|
|
111
111
|
(refresh)="handleRefresh()"
|
|
112
|
-
(accessibilityAction)="emit(accessibilityAction, $event)"
|
|
113
|
-
(accessibilityTap)="emit(accessibilityTap, $event)"
|
|
114
|
-
(magicTap)="emit(magicTap, $event)"
|
|
115
|
-
(accessibilityEscape)="emit(accessibilityEscape, $event)"
|
|
116
112
|
>
|
|
117
113
|
<ng-content></ng-content>
|
|
118
114
|
</symbiote-refresh-control>
|
|
@@ -252,10 +248,19 @@ export class RefreshControl
|
|
|
252
248
|
|
|
253
249
|
// Forward an engine event to the matching @Output(), narrowing the template's untyped $event
|
|
254
250
|
// first. The accessibility* events arrive on the engine's structural event channel.
|
|
255
|
-
emit(emitter: EventEmitter<ISymbioteEvent>, event: unknown): void {
|
|
251
|
+
private emit(emitter: EventEmitter<ISymbioteEvent>, event: unknown): void {
|
|
256
252
|
if (isSymbioteEvent(event)) emitter.emit(event);
|
|
257
253
|
}
|
|
258
254
|
|
|
255
|
+
// The four accessibility events are boolean-GATED Fabric events
|
|
256
|
+
// (`.claude/rules/fabric-boolean-event-gates.md`): a template binding here lit the gate on every
|
|
257
|
+
// instance whether or not an app ever subscribed. `.observed`-gated, mirroring Pressable's.
|
|
258
|
+
private eventEmitterHandler(
|
|
259
|
+
emitter: EventEmitter<ISymbioteEvent>,
|
|
260
|
+
): ((event: unknown) => void) | undefined {
|
|
261
|
+
return emitter.observed ? event => this.emit(emitter, event) : undefined;
|
|
262
|
+
}
|
|
263
|
+
|
|
259
264
|
// The anchor's class-derived style is NOT an @Input: `class="..."`/`[ngClass]` at the use site
|
|
260
265
|
// resolves through the renderer's addClass onto this component's anchor host (see
|
|
261
266
|
// anchorHostStyle's doc comment), so it never shows up in SimpleChanges and ngOnChanges alone
|
|
@@ -294,6 +299,10 @@ export class RefreshControl
|
|
|
294
299
|
nativeID: this.nativeID,
|
|
295
300
|
accessible: this.accessible,
|
|
296
301
|
...this.folded,
|
|
302
|
+
onAccessibilityAction: this.eventEmitterHandler(this.accessibilityAction),
|
|
303
|
+
onAccessibilityTap: this.eventEmitterHandler(this.accessibilityTap),
|
|
304
|
+
onMagicTap: this.eventEmitterHandler(this.magicTap),
|
|
305
|
+
onAccessibilityEscape: this.eventEmitterHandler(this.accessibilityEscape),
|
|
297
306
|
};
|
|
298
307
|
});
|
|
299
308
|
|
|
@@ -45,6 +45,12 @@ import {
|
|
|
45
45
|
export interface IAngularSafeAreaViewProps
|
|
46
46
|
extends IAccessibilityProps, IAriaProps {
|
|
47
47
|
style?: IStyleProp<IViewStyle>;
|
|
48
|
+
// `id` — accepted here and folded to `nativeID` by the shared plan, matching upstream, whose
|
|
49
|
+
// SafeAreaView takes the full ViewProps surface. Added 2026-09-01 with `ID_ALIAS` on the spec
|
|
50
|
+
// entry, deliberately as ONE change: the alias without this prop would make lowering fold a key
|
|
51
|
+
// no spelling of the component accepts, and this prop without the alias would send a raw `id` to
|
|
52
|
+
// Fabric, which declares no such key on any of these views.
|
|
53
|
+
id?: string;
|
|
48
54
|
onLayout?: (event: ISymbioteEvent) => void;
|
|
49
55
|
}
|
|
50
56
|
|
|
@@ -70,14 +76,7 @@ export type IAngularSafeAreaViewInputs = Omit<
|
|
|
70
76
|
imports: [SafeAreaViewHost, SymbioteHostPropsDirective],
|
|
71
77
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
72
78
|
template: `
|
|
73
|
-
<symbiote-safe-area-view
|
|
74
|
-
[symbioteHostProps]="hostProps()"
|
|
75
|
-
(accessibilityAction)="emit(accessibilityAction, $event)"
|
|
76
|
-
(accessibilityTap)="emit(accessibilityTap, $event)"
|
|
77
|
-
(magicTap)="emit(magicTap, $event)"
|
|
78
|
-
(accessibilityEscape)="emit(accessibilityEscape, $event)"
|
|
79
|
-
(layout)="emit(layout, $event)"
|
|
80
|
-
>
|
|
79
|
+
<symbiote-safe-area-view [symbioteHostProps]="hostProps()">
|
|
81
80
|
<ng-content></ng-content>
|
|
82
81
|
</symbiote-safe-area-view>
|
|
83
82
|
`,
|
|
@@ -89,6 +88,11 @@ export class SafeAreaView
|
|
|
89
88
|
// Real @Output()s, not `[onX]="…"` callbacks. Safe to name `layout` the same as the native
|
|
90
89
|
// `layout` event fired inside this component's own template — the engine's bubble() treats
|
|
91
90
|
// ANCHOR_HOST_COMPONENTS as transparent to listener lookup, so there is no double-fire.
|
|
91
|
+
// `layout` plus the four accessibility events are all boolean-GATED Fabric events
|
|
92
|
+
// (`.claude/rules/fabric-boolean-event-gates.md`): native fires them only when the committed
|
|
93
|
+
// payload carries a FUNCTION at that key, so a template binding here lit the gate on every
|
|
94
|
+
// instance whether or not an app ever subscribed. Moved into `hostProps()`'s flat bag below,
|
|
95
|
+
// `.observed`-gated — the shape `Pressable`'s `accessibilityEmitterHandler` already uses.
|
|
92
96
|
@Output() readonly layout = new EventEmitter<ISymbioteEvent>();
|
|
93
97
|
@Output() readonly accessibilityAction = new EventEmitter<ISymbioteEvent>();
|
|
94
98
|
@Output() readonly accessibilityTap = new EventEmitter<ISymbioteEvent>();
|
|
@@ -137,13 +141,20 @@ export class SafeAreaView
|
|
|
137
141
|
// the only ElementRef in the class.
|
|
138
142
|
private readonly elementRef = inject(ElementRef);
|
|
139
143
|
|
|
140
|
-
// Forward an engine event to the matching @Output(), narrowing the
|
|
141
|
-
|
|
142
|
-
// (Angular blocks [onX] property bindings; events flow through (event) only).
|
|
143
|
-
emit(emitter: EventEmitter<ISymbioteEvent>, event: unknown): void {
|
|
144
|
+
// Forward an engine event to the matching @Output(), narrowing the untyped event first.
|
|
145
|
+
private emit(emitter: EventEmitter<ISymbioteEvent>, event: unknown): void {
|
|
144
146
|
if (isSymbioteEvent(event)) emitter.emit(event);
|
|
145
147
|
}
|
|
146
148
|
|
|
149
|
+
// `.observed`-gated, mirroring Pressable's `accessibilityEmitterHandler`: `undefined` here
|
|
150
|
+
// means `setEventListener` never sees a function, so the gated prop's flag never lights
|
|
151
|
+
// (`fabric-boolean-event-gates.md`, "The flag follows the VALUE, not the key").
|
|
152
|
+
private eventEmitterHandler(
|
|
153
|
+
emitter: EventEmitter<ISymbioteEvent>,
|
|
154
|
+
): ((event: unknown) => void) | undefined {
|
|
155
|
+
return emitter.observed ? event => this.emit(emitter, event) : undefined;
|
|
156
|
+
}
|
|
157
|
+
|
|
147
158
|
// Bridges the non-reactive fields `hostProps` reads into the reactive graph, so it can memoize.
|
|
148
159
|
// Plain fields read inside a computed() are UNTRACKED - something must signal "a dependency
|
|
149
160
|
// changed" or the bag goes stale. Signal inputs would do this natively, but `input()` is visible
|
|
@@ -226,6 +237,11 @@ export class SafeAreaView
|
|
|
226
237
|
'aria-valuemin': this.ariaValueMin,
|
|
227
238
|
'aria-valuenow': this.ariaValueNow,
|
|
228
239
|
'aria-valuetext': this.ariaValueText,
|
|
240
|
+
onLayout: this.eventEmitterHandler(this.layout),
|
|
241
|
+
onAccessibilityAction: this.eventEmitterHandler(this.accessibilityAction),
|
|
242
|
+
onAccessibilityTap: this.eventEmitterHandler(this.accessibilityTap),
|
|
243
|
+
onMagicTap: this.eventEmitterHandler(this.magicTap),
|
|
244
|
+
onAccessibilityEscape: this.eventEmitterHandler(this.accessibilityEscape),
|
|
229
245
|
});
|
|
230
246
|
});
|
|
231
247
|
}
|
|
@@ -63,6 +63,11 @@ import {
|
|
|
63
63
|
type IViewStyle,
|
|
64
64
|
} from '@symbiote-native/engine';
|
|
65
65
|
import { anchorHostStyle, SymbioteHostPropsDirective } from '../../primitives';
|
|
66
|
+
import {
|
|
67
|
+
gateWanted,
|
|
68
|
+
injectGateDemand,
|
|
69
|
+
type IGatedAccessibilityEvent,
|
|
70
|
+
} from '../../gate-demand';
|
|
66
71
|
import { RefreshControl } from '../refresh-control';
|
|
67
72
|
import { ScrollViewProjectionController } from './projection';
|
|
68
73
|
|
|
@@ -369,6 +374,10 @@ export abstract class ScrollViewBase
|
|
|
369
374
|
@Output() readonly magicTap = new EventEmitter<ISymbioteEvent>();
|
|
370
375
|
@Output() readonly accessibilityEscape = new EventEmitter<ISymbioteEvent>();
|
|
371
376
|
|
|
377
|
+
// Null unless an adapter wrapper renders this ScrollView in its own template — VirtualizedList
|
|
378
|
+
// does. An app's own `<ScrollView>` gets null and answers from its own `.observed`.
|
|
379
|
+
private readonly gateDemand = injectGateDemand();
|
|
380
|
+
|
|
372
381
|
testID: string | undefined;
|
|
373
382
|
nativeID: string | undefined;
|
|
374
383
|
accessible: boolean | undefined;
|
|
@@ -624,10 +633,14 @@ export abstract class ScrollViewBase
|
|
|
624
633
|
onMomentumScrollBegin: this.onMomentumScrollBegin,
|
|
625
634
|
onMomentumScrollEnd: this.onMomentumScrollEnd,
|
|
626
635
|
onScrollToTop: this.emitterCallback(this.scrollToTop),
|
|
627
|
-
onAccessibilityAction: this.
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
636
|
+
onAccessibilityAction: this.gatedAccessibilityCallback(
|
|
637
|
+
'accessibilityAction',
|
|
638
|
+
),
|
|
639
|
+
onAccessibilityTap: this.gatedAccessibilityCallback('accessibilityTap'),
|
|
640
|
+
onMagicTap: this.gatedAccessibilityCallback('magicTap'),
|
|
641
|
+
onAccessibilityEscape: this.gatedAccessibilityCallback(
|
|
642
|
+
'accessibilityEscape',
|
|
643
|
+
),
|
|
631
644
|
});
|
|
632
645
|
|
|
633
646
|
// RN defaults nested scrolling ON (ScrollView.js `nestedScrollEnabled ?? true`); Android needs
|
|
@@ -814,6 +827,20 @@ export abstract class ScrollViewBase
|
|
|
814
827
|
// subscribes a template's (event) listeners in its CREATION pass, before any input is written and
|
|
815
828
|
// before this component's own view is first refreshed - so `observed` is already settled at the
|
|
816
829
|
// first evaluation and cannot flip afterwards without destroying the whole component.
|
|
830
|
+
// The four GATED accessibility events, which `emitterCallback` cannot answer alone: VirtualizedList
|
|
831
|
+
// renders this component and binds all four, so `.observed` is true on every list in the app
|
|
832
|
+
// whether or not anything listens. A demand from above decides instead — see `gate-demand.ts`.
|
|
833
|
+
//
|
|
834
|
+
// Deliberately NOT applied to the projected RefreshControl's four, one site below: that emitter
|
|
835
|
+
// belongs to a component the APP wrote and projected in, so its `.observed` is honest.
|
|
836
|
+
private gatedAccessibilityCallback(
|
|
837
|
+
name: IGatedAccessibilityEvent,
|
|
838
|
+
): IScrollHandler | undefined {
|
|
839
|
+
const emitter = this[name];
|
|
840
|
+
if (!gateWanted(this.gateDemand, name, emitter)) return undefined;
|
|
841
|
+
return this.emitterCallback(emitter);
|
|
842
|
+
}
|
|
843
|
+
|
|
817
844
|
private emitterCallback(
|
|
818
845
|
emitter: EventEmitter<ISymbioteEvent>,
|
|
819
846
|
): IScrollHandler | undefined {
|