@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,193 @@
|
|
|
1
|
+
// Button for Angular. The minimal cross-platform button in RN's iOS shape (Button.js): a
|
|
2
|
+
// TouchableOpacity wrapping a Text. The base text style, the role constant, and the color fold are
|
|
3
|
+
// shared in @symbiote-native/components/view; here Angular only composes its TouchableOpacity + a
|
|
4
|
+
// symbiote-text child and re-maps the few Button-owned props. The Angular twin of the React/Vue
|
|
5
|
+
// adapter's Button. No JS-side platform branch, so this stays a flat single file.
|
|
6
|
+
//
|
|
7
|
+
// RN's Button fixes accessibilityRole="button", marks the root accessible, and propagates `disabled`
|
|
8
|
+
// into the accessibility state; those three win over any caller value. `title` becomes the Text
|
|
9
|
+
// child; disabled forwards straight; touchSoundDisabled maps to the pressable's android_disableSound.
|
|
10
|
+
// Every OTHER field of the shared IButtonProps (title, color, disabled, touchSoundDisabled, testID,
|
|
11
|
+
// TV-focus, accessibility state) is agnostic and stays RE-EXPORTED from @symbiote-native/components
|
|
12
|
+
// verbatim. onPress and the four accessibility callbacks are the
|
|
13
|
+
// exceptions: each is a real @Output() here (`press`, `accessibilityAction`, `accessibilityTap`,
|
|
14
|
+
// `magicTap`, `accessibilityEscape`), mirroring the Vue adapter's Button, which forks the SAME onPress
|
|
15
|
+
// field the same way (`Omit<ICoreButtonProps, 'onPress'>` + a `press` emit) — not a new precedent, the
|
|
16
|
+
// existing one extended to accessibility.
|
|
17
|
+
|
|
18
|
+
import {
|
|
19
|
+
CUSTOM_ELEMENTS_SCHEMA,
|
|
20
|
+
ChangeDetectionStrategy,
|
|
21
|
+
Component,
|
|
22
|
+
ElementRef,
|
|
23
|
+
EventEmitter,
|
|
24
|
+
inject,
|
|
25
|
+
Input,
|
|
26
|
+
Output,
|
|
27
|
+
} from '@angular/core';
|
|
28
|
+
import {
|
|
29
|
+
BUTTON_ACCESSIBILITY_ROLE,
|
|
30
|
+
resolveButtonTextStyle,
|
|
31
|
+
type IAccessibilityProps,
|
|
32
|
+
type IAccessibilityStateValue,
|
|
33
|
+
type IAriaProps,
|
|
34
|
+
type IButtonProps as ICoreButtonProps,
|
|
35
|
+
} from '@symbiote-native/components';
|
|
36
|
+
import type { ISymbioteEvent, ITextStyle, IStyleProp, IViewStyle } from '@symbiote-native/engine';
|
|
37
|
+
import { anchorStyleProp, TextHost } from '../primitives';
|
|
38
|
+
import { TouchableOpacity } from './touchable';
|
|
39
|
+
|
|
40
|
+
export type IButtonProps = Omit<
|
|
41
|
+
ICoreButtonProps,
|
|
42
|
+
| 'onPress'
|
|
43
|
+
| 'onAccessibilityAction'
|
|
44
|
+
| 'onAccessibilityTap'
|
|
45
|
+
| 'onMagicTap'
|
|
46
|
+
| 'onAccessibilityEscape'
|
|
47
|
+
>;
|
|
48
|
+
|
|
49
|
+
@Component({
|
|
50
|
+
selector: 'Button',
|
|
51
|
+
standalone: true,
|
|
52
|
+
schemas: [CUSTOM_ELEMENTS_SCHEMA],
|
|
53
|
+
imports: [TouchableOpacity, TextHost],
|
|
54
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
55
|
+
template: `
|
|
56
|
+
<TouchableOpacity
|
|
57
|
+
[style]="anchorStyle"
|
|
58
|
+
(press)="press.emit($event)"
|
|
59
|
+
[disabled]="disabled"
|
|
60
|
+
[android_disableSound]="touchSoundDisabled"
|
|
61
|
+
[accessibilityRole]="buttonRole"
|
|
62
|
+
[accessible]="true"
|
|
63
|
+
[accessibilityState]="{ disabled }"
|
|
64
|
+
[testID]="testID"
|
|
65
|
+
[nativeID]="nativeID"
|
|
66
|
+
[hasTVPreferredFocus]="hasTVPreferredFocus"
|
|
67
|
+
[nextFocusDown]="nextFocusDown"
|
|
68
|
+
[nextFocusForward]="nextFocusForward"
|
|
69
|
+
[nextFocusLeft]="nextFocusLeft"
|
|
70
|
+
[nextFocusRight]="nextFocusRight"
|
|
71
|
+
[nextFocusUp]="nextFocusUp"
|
|
72
|
+
[accessibilityLabel]="accessibilityLabel"
|
|
73
|
+
[accessibilityHint]="accessibilityHint"
|
|
74
|
+
[accessibilityValue]="accessibilityValue"
|
|
75
|
+
[accessibilityActions]="accessibilityActions"
|
|
76
|
+
[accessibilityLabelledBy]="accessibilityLabelledBy"
|
|
77
|
+
[importantForAccessibility]="importantForAccessibility"
|
|
78
|
+
[accessibilityLiveRegion]="accessibilityLiveRegion"
|
|
79
|
+
[screenReaderFocusable]="screenReaderFocusable"
|
|
80
|
+
[accessibilityViewIsModal]="accessibilityViewIsModal"
|
|
81
|
+
[accessibilityElementsHidden]="accessibilityElementsHidden"
|
|
82
|
+
[accessibilityIgnoresInvertColors]="accessibilityIgnoresInvertColors"
|
|
83
|
+
[accessibilityLanguage]="accessibilityLanguage"
|
|
84
|
+
[accessibilityRespondsToUserInteraction]="accessibilityRespondsToUserInteraction"
|
|
85
|
+
[accessibilityShowsLargeContentViewer]="accessibilityShowsLargeContentViewer"
|
|
86
|
+
[accessibilityLargeContentTitle]="accessibilityLargeContentTitle"
|
|
87
|
+
(accessibilityAction)="accessibilityAction.emit($event)"
|
|
88
|
+
(accessibilityTap)="accessibilityTap.emit($event)"
|
|
89
|
+
(magicTap)="magicTap.emit($event)"
|
|
90
|
+
(accessibilityEscape)="accessibilityEscape.emit($event)"
|
|
91
|
+
[ariaLabel]="ariaLabel"
|
|
92
|
+
[ariaBusy]="ariaBusy"
|
|
93
|
+
[ariaChecked]="ariaChecked"
|
|
94
|
+
[ariaDisabled]="ariaDisabled"
|
|
95
|
+
[ariaExpanded]="ariaExpanded"
|
|
96
|
+
[ariaHidden]="ariaHidden"
|
|
97
|
+
[ariaLabelledBy]="ariaLabelledBy"
|
|
98
|
+
[ariaLive]="ariaLive"
|
|
99
|
+
[ariaSelected]="ariaSelected"
|
|
100
|
+
[ariaModal]="ariaModal"
|
|
101
|
+
[ariaValueMax]="ariaValueMax"
|
|
102
|
+
[ariaValueMin]="ariaValueMin"
|
|
103
|
+
[ariaValueNow]="ariaValueNow"
|
|
104
|
+
[ariaValueText]="ariaValueText"
|
|
105
|
+
[role]="role"
|
|
106
|
+
>
|
|
107
|
+
<symbiote-text [style]="textStyle">{{ title }}</symbiote-text>
|
|
108
|
+
</TouchableOpacity>
|
|
109
|
+
`,
|
|
110
|
+
})
|
|
111
|
+
export class Button implements IButtonProps {
|
|
112
|
+
// This component's OWN host — the non-painting anchor `class="..."` at the use site resolves
|
|
113
|
+
// onto (see anchorHostStyle's doc comment in primitives/shared.ts) — NOT the inner
|
|
114
|
+
// TouchableOpacity/Pressable one level down. RN's real Button takes no `style` prop, so this
|
|
115
|
+
// is the only style source forwarded.
|
|
116
|
+
private readonly elementRef = inject(ElementRef);
|
|
117
|
+
|
|
118
|
+
@Output() readonly press = new EventEmitter<ISymbioteEvent>();
|
|
119
|
+
@Output() readonly accessibilityAction = new EventEmitter<ISymbioteEvent>();
|
|
120
|
+
@Output() readonly accessibilityTap = new EventEmitter<ISymbioteEvent>();
|
|
121
|
+
@Output() readonly magicTap = new EventEmitter<ISymbioteEvent>();
|
|
122
|
+
@Output() readonly accessibilityEscape = new EventEmitter<ISymbioteEvent>();
|
|
123
|
+
|
|
124
|
+
// Button-owned props: title is the Text child, color/disabled fold into the label style, and
|
|
125
|
+
// touchSoundDisabled re-maps onto TouchableOpacity.
|
|
126
|
+
@Input() title = '';
|
|
127
|
+
@Input() color?: string;
|
|
128
|
+
@Input() disabled?: boolean;
|
|
129
|
+
@Input() touchSoundDisabled?: boolean;
|
|
130
|
+
@Input() testID?: string;
|
|
131
|
+
|
|
132
|
+
// TV-focus props (Button.js): inert on a phone host, forwarded through TouchableOpacity.
|
|
133
|
+
@Input() hasTVPreferredFocus?: boolean;
|
|
134
|
+
@Input() nextFocusDown?: number;
|
|
135
|
+
@Input() nextFocusForward?: number;
|
|
136
|
+
@Input() nextFocusLeft?: number;
|
|
137
|
+
@Input() nextFocusRight?: number;
|
|
138
|
+
@Input() nextFocusUp?: number;
|
|
139
|
+
|
|
140
|
+
// Accessibility surface (IAccessibilityProps). accessibilityRole, accessible, and
|
|
141
|
+
// accessibilityState are declared for surface completeness but Button forces its own
|
|
142
|
+
// (button / true / { disabled }); the caller value is ignored, mirroring RN's Button.js.
|
|
143
|
+
@Input() nativeID?: string;
|
|
144
|
+
@Input() accessible?: boolean;
|
|
145
|
+
@Input() accessibilityLabel?: string;
|
|
146
|
+
@Input() accessibilityHint?: string;
|
|
147
|
+
@Input() accessibilityRole?: IAccessibilityProps['accessibilityRole'];
|
|
148
|
+
@Input() accessibilityState?: IAccessibilityStateValue;
|
|
149
|
+
@Input() accessibilityValue?: IAccessibilityProps['accessibilityValue'];
|
|
150
|
+
@Input() accessibilityActions?: IAccessibilityProps['accessibilityActions'];
|
|
151
|
+
@Input() accessibilityLabelledBy?: string | string[];
|
|
152
|
+
@Input() importantForAccessibility?: IAccessibilityProps['importantForAccessibility'];
|
|
153
|
+
@Input() accessibilityLiveRegion?: IAccessibilityProps['accessibilityLiveRegion'];
|
|
154
|
+
@Input() screenReaderFocusable?: boolean;
|
|
155
|
+
@Input() accessibilityViewIsModal?: boolean;
|
|
156
|
+
@Input() accessibilityElementsHidden?: boolean;
|
|
157
|
+
@Input() accessibilityIgnoresInvertColors?: boolean;
|
|
158
|
+
@Input() accessibilityLanguage?: string;
|
|
159
|
+
@Input() accessibilityRespondsToUserInteraction?: boolean;
|
|
160
|
+
@Input() accessibilityShowsLargeContentViewer?: boolean;
|
|
161
|
+
@Input() accessibilityLargeContentTitle?: string;
|
|
162
|
+
|
|
163
|
+
// Web-alias a11y surface (IAriaProps). External dashed aliases are exposed as Angular inputs and
|
|
164
|
+
// forwarded to TouchableOpacity's camelCase aria inputs.
|
|
165
|
+
@Input() role?: IAriaProps['role'];
|
|
166
|
+
@Input('aria-label') ariaLabel?: string;
|
|
167
|
+
@Input('aria-labelledby') ariaLabelledBy?: string;
|
|
168
|
+
@Input('aria-live') ariaLive?: IAriaProps['aria-live'];
|
|
169
|
+
@Input('aria-hidden') ariaHidden?: boolean;
|
|
170
|
+
@Input('aria-busy') ariaBusy?: boolean;
|
|
171
|
+
@Input('aria-checked') ariaChecked?: boolean | 'mixed';
|
|
172
|
+
@Input('aria-disabled') ariaDisabled?: boolean;
|
|
173
|
+
@Input('aria-expanded') ariaExpanded?: boolean;
|
|
174
|
+
@Input('aria-selected') ariaSelected?: boolean;
|
|
175
|
+
@Input('aria-modal') ariaModal?: boolean;
|
|
176
|
+
@Input('aria-valuemax') ariaValueMax?: number;
|
|
177
|
+
@Input('aria-valuemin') ariaValueMin?: number;
|
|
178
|
+
@Input('aria-valuenow') ariaValueNow?: number;
|
|
179
|
+
@Input('aria-valuetext') ariaValueText?: string;
|
|
180
|
+
|
|
181
|
+
// RN's Button is always accessibilityRole="button"; the shared role constant, not a literal.
|
|
182
|
+
readonly buttonRole = BUTTON_ACCESSIBILITY_ROLE;
|
|
183
|
+
|
|
184
|
+
// The label style with the caller color tinted in and `disabled` greying it out (disabled wins),
|
|
185
|
+
// computed by the shared fold so every adapter paints the identical button.
|
|
186
|
+
get textStyle(): ITextStyle {
|
|
187
|
+
return resolveButtonTextStyle(this.color, this.disabled);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
get anchorStyle(): IStyleProp<IViewStyle> | undefined {
|
|
191
|
+
return anchorStyleProp<IViewStyle>(this.elementRef);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
// Regression guard: columnWrapperStyle previously accepted ONLY a JS style object/array. It now
|
|
2
|
+
// also resolves a class-name string through the shared style registry, merged onto the same
|
|
3
|
+
// flex-row wrapper as an object/array value (see index.ts's ngOnChanges). Mirrors the Vue twin
|
|
4
|
+
// (column-wrapper-style-class.test.ts) and flat-list.test.ts's row-packing shape.
|
|
5
|
+
import '@angular/compiler';
|
|
6
|
+
import { Component } from '@angular/core';
|
|
7
|
+
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
|
8
|
+
import { clearGlobalStyles, registerStyles } from '@symbiote-native/engine';
|
|
9
|
+
import { installFabric, type IFakeNode } from '@symbiote-native/test-utils';
|
|
10
|
+
|
|
11
|
+
import { mount, unmount } from '../../render';
|
|
12
|
+
import { FlatList } from './index';
|
|
13
|
+
import { VListItemDirective } from '../virtualized-list/directives';
|
|
14
|
+
|
|
15
|
+
const ROOT_TAG = 953;
|
|
16
|
+
const fabric = installFabric();
|
|
17
|
+
const tick = (): Promise<void> => new Promise(resolve => setTimeout(resolve, 0));
|
|
18
|
+
|
|
19
|
+
interface IRow {
|
|
20
|
+
id: string;
|
|
21
|
+
label: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const rows: IRow[] = Array.from({ length: 4 }, (_unused, index) => ({
|
|
25
|
+
id: `r-${index}`,
|
|
26
|
+
label: `row-${index}`,
|
|
27
|
+
}));
|
|
28
|
+
|
|
29
|
+
function walk(nodes: IFakeNode[], visit: (node: IFakeNode) => void): void {
|
|
30
|
+
for (const node of nodes) {
|
|
31
|
+
visit(node);
|
|
32
|
+
walk(node.children, visit);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function rowsWithFlexDirection(): IFakeNode[] {
|
|
37
|
+
const found: IFakeNode[] = [];
|
|
38
|
+
walk(fabric.committed, node => {
|
|
39
|
+
if (node.viewName === 'RCTView' && node.props.flexDirection === 'row') found.push(node);
|
|
40
|
+
});
|
|
41
|
+
return found;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
@Component({
|
|
45
|
+
selector: 'symbiote-flatlist-column-style-class-host',
|
|
46
|
+
standalone: true,
|
|
47
|
+
imports: [FlatList, VListItemDirective],
|
|
48
|
+
template: `
|
|
49
|
+
<FlatList
|
|
50
|
+
[data]="rows"
|
|
51
|
+
[numColumns]="2"
|
|
52
|
+
[keyExtractor]="keyExtractor"
|
|
53
|
+
[columnWrapperStyle]="'gap8'"
|
|
54
|
+
>
|
|
55
|
+
<ng-template vListItem let-item>
|
|
56
|
+
<symbiote-text [testID]="item.id">{{ item.label }}</symbiote-text>
|
|
57
|
+
</ng-template>
|
|
58
|
+
</FlatList>
|
|
59
|
+
`,
|
|
60
|
+
})
|
|
61
|
+
class FlatListColumnStyleClassHost {
|
|
62
|
+
rows = rows;
|
|
63
|
+
keyExtractor = (item: IRow): string => item.id;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
@Component({
|
|
67
|
+
selector: 'symbiote-flatlist-column-style-object-host',
|
|
68
|
+
standalone: true,
|
|
69
|
+
imports: [FlatList, VListItemDirective],
|
|
70
|
+
template: `
|
|
71
|
+
<FlatList
|
|
72
|
+
[data]="rows"
|
|
73
|
+
[numColumns]="2"
|
|
74
|
+
[keyExtractor]="keyExtractor"
|
|
75
|
+
[columnWrapperStyle]="{ gap: 4 }"
|
|
76
|
+
>
|
|
77
|
+
<ng-template vListItem let-item>
|
|
78
|
+
<symbiote-text [testID]="item.id">{{ item.label }}</symbiote-text>
|
|
79
|
+
</ng-template>
|
|
80
|
+
</FlatList>
|
|
81
|
+
`,
|
|
82
|
+
})
|
|
83
|
+
class FlatListColumnStyleObjectHost {
|
|
84
|
+
rows = rows;
|
|
85
|
+
keyExtractor = (item: IRow): string => item.id;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
beforeEach(() => {
|
|
89
|
+
fabric.reset();
|
|
90
|
+
registerStyles({ gap8: { gap: 8 } });
|
|
91
|
+
});
|
|
92
|
+
afterEach(() => {
|
|
93
|
+
unmount(ROOT_TAG);
|
|
94
|
+
clearGlobalStyles();
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
describe('FlatList columnWrapperStyle class-name support', () => {
|
|
98
|
+
it('resolves a class-name string onto the row wrapper alongside flexDirection', async () => {
|
|
99
|
+
mount(ROOT_TAG, FlatListColumnStyleClassHost);
|
|
100
|
+
await tick();
|
|
101
|
+
await tick();
|
|
102
|
+
|
|
103
|
+
const found = rowsWithFlexDirection();
|
|
104
|
+
expect(found.length, 'two flex-row rows for 4 items in 2 columns').toBe(2);
|
|
105
|
+
expect(found[0].props.gap).toBe(8);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it('still accepts an ordinary style object unchanged', async () => {
|
|
109
|
+
mount(ROOT_TAG, FlatListColumnStyleObjectHost);
|
|
110
|
+
await tick();
|
|
111
|
+
await tick();
|
|
112
|
+
|
|
113
|
+
const found = rowsWithFlexDirection();
|
|
114
|
+
expect(found[0].props.gap).toBe(4);
|
|
115
|
+
});
|
|
116
|
+
});
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import '@angular/compiler';
|
|
2
|
+
import { Component } from '@angular/core';
|
|
3
|
+
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
|
4
|
+
import { installFabric } from '@symbiote-native/test-utils';
|
|
5
|
+
import { flattenStyle } from '@symbiote-native/engine';
|
|
6
|
+
|
|
7
|
+
import { mount, unmount } from '../../render';
|
|
8
|
+
import { FlatList } from './index';
|
|
9
|
+
import { VListItemDirective } from '../virtualized-list/directives';
|
|
10
|
+
|
|
11
|
+
const ROOT_TAG = 905;
|
|
12
|
+
const fabric = installFabric();
|
|
13
|
+
|
|
14
|
+
interface IChip {
|
|
15
|
+
id: string;
|
|
16
|
+
n: number;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const chips: IChip[] = Array.from({ length: 24 }, (_unused, index) => ({
|
|
20
|
+
id: `c-${index}`,
|
|
21
|
+
n: index,
|
|
22
|
+
}));
|
|
23
|
+
|
|
24
|
+
// Regression coverage for a real device bug: Angular compiles a literal `[style]="…"` binding to
|
|
25
|
+
// the built-in ɵɵstyleMap instruction (NOT a regular @Input() property write), regardless of the
|
|
26
|
+
// target's declared type — it only understands a flat style object, never an array. RN's own
|
|
27
|
+
// `style={[base, override]}` composition idiom (used by examples/angular/App.ts's "FlatList · 24
|
|
28
|
+
// chips" demo for the per-item computed backgroundColor) crashed deep inside Angular's own styling
|
|
29
|
+
// engine (`prop.indexOf is not a function`), and the crash landed inside a zoneless
|
|
30
|
+
// SymbioteChangeDetectionScheduler tick with no catch around it — so the retry kept re-firing
|
|
31
|
+
// forever (RAM climbing, constant re-render log spam on device) while the item never painted its
|
|
32
|
+
// style. Fixed by flattening any array-composed style with the engine's own `flattenStyle` before
|
|
33
|
+
// it reaches a `[style]=` binding — required of app authors (see App.ts) AND of every adapter
|
|
34
|
+
// component that forwards its own `style` @Input() onward (see the `resolvedStyle` getters/fields
|
|
35
|
+
// in virtualized-list, section-list, virtualized-section-list — flat-list's own `rowStyle` had the
|
|
36
|
+
// identical landmine, always assigning an array).
|
|
37
|
+
@Component({
|
|
38
|
+
selector: 'symbiote-chip-list-host',
|
|
39
|
+
standalone: true,
|
|
40
|
+
imports: [FlatList, VListItemDirective],
|
|
41
|
+
template: `
|
|
42
|
+
<FlatList
|
|
43
|
+
testID="chips-list"
|
|
44
|
+
[data]="chips"
|
|
45
|
+
[horizontal]="true"
|
|
46
|
+
[keyExtractor]="keyExtractor"
|
|
47
|
+
[getItemLayout]="getItemLayout"
|
|
48
|
+
[style]="{ height: 84 }"
|
|
49
|
+
>
|
|
50
|
+
<ng-template vListItem let-item>
|
|
51
|
+
<symbiote-view
|
|
52
|
+
[testID]="'chip-' + item.n"
|
|
53
|
+
[style]="flattenStyle([{ height: 72, width: 64 }, { backgroundColor: chipColor(item) }])"
|
|
54
|
+
>
|
|
55
|
+
<symbiote-text>{{ item.n }}</symbiote-text>
|
|
56
|
+
</symbiote-view>
|
|
57
|
+
</ng-template>
|
|
58
|
+
</FlatList>
|
|
59
|
+
`,
|
|
60
|
+
})
|
|
61
|
+
class ChipListHost {
|
|
62
|
+
chips = chips;
|
|
63
|
+
flattenStyle = flattenStyle;
|
|
64
|
+
keyExtractor = (item: IChip): string => item.id;
|
|
65
|
+
getItemLayout = (
|
|
66
|
+
_data: unknown,
|
|
67
|
+
index: number,
|
|
68
|
+
): { length: number; offset: number; index: number } => ({
|
|
69
|
+
length: 64,
|
|
70
|
+
offset: 64 * index,
|
|
71
|
+
index,
|
|
72
|
+
});
|
|
73
|
+
chipColor(item: IChip): string {
|
|
74
|
+
return `hsl(${(item.n * 47) % 360}, 70%, 50%)`;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
beforeEach(() => fabric.reset());
|
|
79
|
+
afterEach(() => unmount(ROOT_TAG));
|
|
80
|
+
|
|
81
|
+
function findCommitted(
|
|
82
|
+
nodes: typeof fabric.committed,
|
|
83
|
+
predicate: (n: (typeof nodes)[number]) => boolean,
|
|
84
|
+
): (typeof nodes)[number] | undefined {
|
|
85
|
+
for (const node of nodes) {
|
|
86
|
+
if (predicate(node)) return node;
|
|
87
|
+
const found = findCommitted(node.children, predicate);
|
|
88
|
+
if (found !== undefined) return found;
|
|
89
|
+
}
|
|
90
|
+
return undefined;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
describe('FlatList array-composed item style', () => {
|
|
94
|
+
it('flattens to a plain object instead of crashing ɵɵstyleMap, and commits it', async () => {
|
|
95
|
+
mount(ROOT_TAG, ChipListHost);
|
|
96
|
+
await new Promise<void>(resolve => setTimeout(resolve, 0));
|
|
97
|
+
await new Promise<void>(resolve => setTimeout(resolve, 0));
|
|
98
|
+
|
|
99
|
+
const chip0 = findCommitted(fabric.committed, node => node.props.testID === 'chip-0');
|
|
100
|
+
expect(chip0?.props).toMatchObject({
|
|
101
|
+
height: 72,
|
|
102
|
+
width: 64,
|
|
103
|
+
backgroundColor: 'hsl(0, 70%, 50%)',
|
|
104
|
+
});
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it('does not free-run change detection once the window settles', async () => {
|
|
108
|
+
mount(ROOT_TAG, ChipListHost);
|
|
109
|
+
await new Promise<void>(resolve => setTimeout(resolve, 0));
|
|
110
|
+
await new Promise<void>(resolve => setTimeout(resolve, 0));
|
|
111
|
+
|
|
112
|
+
const scroll = fabric.find(node => node.viewName === 'RCTScrollView');
|
|
113
|
+
if (scroll !== undefined) {
|
|
114
|
+
fabric.fireEvent(scroll.instanceHandle, 'topLayout', {
|
|
115
|
+
layout: { x: 0, y: 0, width: 360, height: 84 },
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
await new Promise<void>(resolve => setTimeout(resolve, 0));
|
|
119
|
+
await new Promise<void>(resolve => setTimeout(resolve, 0));
|
|
120
|
+
|
|
121
|
+
let previous = fabric.counts.completeRoot;
|
|
122
|
+
let stillGrowing = false;
|
|
123
|
+
for (let tick = 0; tick < 10; tick += 1) {
|
|
124
|
+
await new Promise<void>(resolve => setTimeout(resolve, 0));
|
|
125
|
+
const current = fabric.counts.completeRoot;
|
|
126
|
+
if (current > previous) stillGrowing = true;
|
|
127
|
+
previous = current;
|
|
128
|
+
}
|
|
129
|
+
expect(stillGrowing).toBe(false);
|
|
130
|
+
});
|
|
131
|
+
});
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
// Regression test for the anchor/class bug: FlatList is its own
|
|
2
|
+
// ANCHOR_HOST_COMPONENTS entry, separate from the VirtualizedList/ScrollView anchors it wraps two
|
|
3
|
+
// levels down — a class= on <FlatList> resolves onto FlatList's OWN anchor and needs its OWN
|
|
4
|
+
// anchorHostStyle merge (see index.ts's ngOnChanges), it is NOT transitively fixed by
|
|
5
|
+
// VirtualizedList's or ScrollView's own fixes. Mirrors pressable.test.ts's "resolves a class=" case.
|
|
6
|
+
import '@angular/compiler';
|
|
7
|
+
import { Component } from '@angular/core';
|
|
8
|
+
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
|
9
|
+
import { clearGlobalStyles, registerStyles } from '@symbiote-native/engine';
|
|
10
|
+
import { installFabric } from '@symbiote-native/test-utils';
|
|
11
|
+
|
|
12
|
+
import { mount, unmount } from '../../render';
|
|
13
|
+
import { FlatList } from './index';
|
|
14
|
+
import { VListItemDirective } from '../virtualized-list/directives';
|
|
15
|
+
|
|
16
|
+
const ROOT_TAG = 951;
|
|
17
|
+
const fabric = installFabric();
|
|
18
|
+
const tick = (): Promise<void> => new Promise(resolve => setTimeout(resolve, 0));
|
|
19
|
+
|
|
20
|
+
interface IRow {
|
|
21
|
+
id: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const rows: IRow[] = [{ id: 'a' }, { id: 'b' }];
|
|
25
|
+
|
|
26
|
+
@Component({
|
|
27
|
+
selector: 'symbiote-flatlist-class-host',
|
|
28
|
+
standalone: true,
|
|
29
|
+
imports: [FlatList, VListItemDirective],
|
|
30
|
+
template: `
|
|
31
|
+
<FlatList
|
|
32
|
+
class="card"
|
|
33
|
+
[data]="rows"
|
|
34
|
+
[keyExtractor]="keyExtractor"
|
|
35
|
+
[getItemLayout]="getItemLayout"
|
|
36
|
+
>
|
|
37
|
+
<ng-template vListItem let-item>
|
|
38
|
+
<symbiote-text [testID]="item.id">{{ item.id }}</symbiote-text>
|
|
39
|
+
</ng-template>
|
|
40
|
+
</FlatList>
|
|
41
|
+
`,
|
|
42
|
+
})
|
|
43
|
+
class FlatListClassHost {
|
|
44
|
+
rows = rows;
|
|
45
|
+
keyExtractor = (item: IRow): string => item.id;
|
|
46
|
+
getItemLayout = (
|
|
47
|
+
_data: unknown,
|
|
48
|
+
index: number,
|
|
49
|
+
): { length: number; offset: number; index: number } => ({
|
|
50
|
+
length: 30,
|
|
51
|
+
offset: 30 * index,
|
|
52
|
+
index,
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
beforeEach(() => {
|
|
57
|
+
fabric.reset();
|
|
58
|
+
registerStyles({ card: { backgroundColor: 'red' } });
|
|
59
|
+
});
|
|
60
|
+
afterEach(() => {
|
|
61
|
+
unmount(ROOT_TAG);
|
|
62
|
+
clearGlobalStyles();
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
describe('FlatList anchor class= resolution', () => {
|
|
66
|
+
it('resolves a class= on the FlatList use site onto the real committed scroll host', async () => {
|
|
67
|
+
mount(ROOT_TAG, FlatListClassHost);
|
|
68
|
+
await tick();
|
|
69
|
+
await tick();
|
|
70
|
+
|
|
71
|
+
const node = fabric.find(n => n.props.backgroundColor === 'red');
|
|
72
|
+
expect(node, 'a real Fabric node carries the class-derived style').toBeDefined();
|
|
73
|
+
});
|
|
74
|
+
});
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import '@angular/compiler';
|
|
2
|
+
import { Component } from '@angular/core';
|
|
3
|
+
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
|
4
|
+
import { installFabric } from '@symbiote-native/test-utils';
|
|
5
|
+
|
|
6
|
+
import { mount, unmount } from '../../render';
|
|
7
|
+
import { FlatList } from './index';
|
|
8
|
+
import { VListItemDirective } from '../virtualized-list/directives';
|
|
9
|
+
|
|
10
|
+
const ROOT_TAG = 907;
|
|
11
|
+
const fabric = installFabric();
|
|
12
|
+
|
|
13
|
+
interface IChip {
|
|
14
|
+
id: string;
|
|
15
|
+
n: number;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const chips: IChip[] = Array.from({ length: 24 }, (_unused, index) => ({
|
|
19
|
+
id: `c-${index}`,
|
|
20
|
+
n: index,
|
|
21
|
+
}));
|
|
22
|
+
|
|
23
|
+
// Mirrors examples/angular/App.ts's "FlatList · 24 chips" demo end to end: a horizontal FlatList
|
|
24
|
+
// with getItemLayout. Regression coverage for a bug fixed 2026-07: cell content used to land
|
|
25
|
+
// OUTSIDE the ScrollView's content view entirely — as top-level siblings
|
|
26
|
+
// of the committed RCTScrollView node, instead of children of its RCTScrollContentView — which is
|
|
27
|
+
// why the device screenshot showed full-width vertically-stacked chips instead of a small
|
|
28
|
+
// horizontal row. Root cause was `<ng-content>` declared once per `@if`/`@else` branch in
|
|
29
|
+
// scroll-view/index.ios.ts (a documented Angular limitation, angular/angular#53310); fixed by
|
|
30
|
+
// collapsing to a single unconditional host tag since iOS never needed axis-specific tags to begin
|
|
31
|
+
// with (`horizontal` already flows as a plain prop). This test pins the CORRECT tree shape.
|
|
32
|
+
@Component({
|
|
33
|
+
selector: 'symbiote-chip-container-host',
|
|
34
|
+
standalone: true,
|
|
35
|
+
imports: [FlatList, VListItemDirective],
|
|
36
|
+
template: `
|
|
37
|
+
<FlatList
|
|
38
|
+
testID="chips-list"
|
|
39
|
+
[data]="chips"
|
|
40
|
+
[horizontal]="true"
|
|
41
|
+
[keyExtractor]="keyExtractor"
|
|
42
|
+
[getItemLayout]="getItemLayout"
|
|
43
|
+
[style]="chipListStyle"
|
|
44
|
+
>
|
|
45
|
+
<ng-template vListItem let-item>
|
|
46
|
+
<symbiote-view [testID]="'chip-' + item.n" [style]="chipCardStyle">
|
|
47
|
+
<symbiote-text>{{ item.n }}</symbiote-text>
|
|
48
|
+
</symbiote-view>
|
|
49
|
+
</ng-template>
|
|
50
|
+
</FlatList>
|
|
51
|
+
`,
|
|
52
|
+
})
|
|
53
|
+
class ChipContainerHost {
|
|
54
|
+
chips = chips;
|
|
55
|
+
chipListStyle = { height: 84 };
|
|
56
|
+
chipCardStyle = { width: 72, height: 64, borderRadius: 14 };
|
|
57
|
+
keyExtractor = (item: IChip): string => item.id;
|
|
58
|
+
getItemLayout = (
|
|
59
|
+
_data: unknown,
|
|
60
|
+
index: number,
|
|
61
|
+
): { length: number; offset: number; index: number } => ({
|
|
62
|
+
length: 64,
|
|
63
|
+
offset: 64 * index,
|
|
64
|
+
index,
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
beforeEach(() => fabric.reset());
|
|
69
|
+
afterEach(() => unmount(ROOT_TAG));
|
|
70
|
+
|
|
71
|
+
function findWithin(
|
|
72
|
+
root: (typeof fabric.committed)[number],
|
|
73
|
+
predicate: (n: (typeof fabric.committed)[number]) => boolean,
|
|
74
|
+
): (typeof fabric.committed)[number] | undefined {
|
|
75
|
+
if (predicate(root)) return root;
|
|
76
|
+
for (const child of root.children) {
|
|
77
|
+
const found = findWithin(child, predicate);
|
|
78
|
+
if (found !== undefined) return found;
|
|
79
|
+
}
|
|
80
|
+
return undefined;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function findTop(
|
|
84
|
+
nodes: typeof fabric.committed,
|
|
85
|
+
predicate: (n: (typeof nodes)[number]) => boolean,
|
|
86
|
+
): (typeof nodes)[number] | undefined {
|
|
87
|
+
for (const node of nodes) {
|
|
88
|
+
const found = findWithin(node, predicate);
|
|
89
|
+
if (found !== undefined) return found;
|
|
90
|
+
}
|
|
91
|
+
return undefined;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
describe('FlatList cells stay inside the ScrollView content view', () => {
|
|
95
|
+
it('nests chip cells under RCTScrollContentView, not as siblings of RCTScrollView', async () => {
|
|
96
|
+
mount(ROOT_TAG, ChipContainerHost);
|
|
97
|
+
await new Promise<void>(resolve => setTimeout(resolve, 0));
|
|
98
|
+
await new Promise<void>(resolve => setTimeout(resolve, 0));
|
|
99
|
+
|
|
100
|
+
const scroll = findTop(fabric.committed, node => node.viewName === 'RCTScrollView');
|
|
101
|
+
expect(scroll).toBeDefined();
|
|
102
|
+
if (scroll === undefined) return;
|
|
103
|
+
|
|
104
|
+
const chip0InsideScroll = findWithin(scroll, node => node.props.testID === 'chip-0');
|
|
105
|
+
expect(chip0InsideScroll).toBeDefined();
|
|
106
|
+
});
|
|
107
|
+
});
|