@symbiote-native/svelte 3.0.0 → 3.0.2

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.
@@ -17,6 +17,7 @@
17
17
  expandRowViewability,
18
18
  firstItemOfRow,
19
19
  lastItemOfRow,
20
+ removeClippedSubviewsOrDefault,
20
21
  rowKeyExtractor,
21
22
  type IRow,
22
23
  type ISeparatorProps,
@@ -24,6 +25,7 @@
24
25
  type IViewableItemsChangedInfo,
25
26
  } from '@symbiote-native/components';
26
27
  import {
28
+ Platform,
27
29
  resolveClassName,
28
30
  type ISymbioteNode,
29
31
  } from '@symbiote-native/engine';
@@ -38,6 +40,11 @@
38
40
 
39
41
  let props: IProps<ItemT> = $props();
40
42
 
43
+ // FlatList.js always sends it, defaulted per platform.
44
+ const removeClippedSubviews = $derived(
45
+ removeClippedSubviewsOrDefault(props.removeClippedSubviews, Platform.OS),
46
+ );
47
+
41
48
  // $state.raw: the inner VirtualizedList component instance (its exports), held by identity —
42
49
  // same concern as every other handle in this adapter (Switch's hostShim, VirtualizedList's own
43
50
  // scrollHandle over its ShimElement).
@@ -238,6 +245,8 @@
238
245
  scrollEventThrottle={props.scrollEventThrottle}
239
246
  keyboardShouldPersistTaps={props.keyboardShouldPersistTaps}
240
247
  keyboardDismissMode={props.keyboardDismissMode}
248
+ {removeClippedSubviews}
249
+ nestedScrollEnabled={props.nestedScrollEnabled}
241
250
  style={props.style}
242
251
  contentContainerStyle={props.contentContainerStyle}
243
252
  class={props.class}
@@ -286,6 +295,8 @@
286
295
  scrollEventThrottle={props.scrollEventThrottle}
287
296
  keyboardShouldPersistTaps={props.keyboardShouldPersistTaps}
288
297
  keyboardDismissMode={props.keyboardDismissMode}
298
+ {removeClippedSubviews}
299
+ nestedScrollEnabled={props.nestedScrollEnabled}
289
300
  style={props.style}
290
301
  contentContainerStyle={props.contentContainerStyle}
291
302
  class={props.class}
@@ -6,9 +6,7 @@
6
6
  // @symbiote-native/components and are shared verbatim with React/Vue (core/components/src/
7
7
  // state/modal.ts + view/render-modal.ts); Svelte supplies only the lifecycle:
8
8
  // - $state over the keep-alive reducer (switchReducer's twin: modalReducer)
9
- // - a POST-render $effect driving the visible->hidden transition (Svelte's $effect runs
10
- // after the DOM update, the same "one keep-alive frame survives" timing as React's
11
- // useEffect / Vue's flush:'post' watch)
9
+ // - a $effect arming the keep-alive on show, and the native dismiss dropping it (iOS)
12
10
  // - the descriptor bridge: renderModal() always paints the SAME fixed shape (one
13
11
  // modal host wrapping one view container — only prop VALUES vary,
14
12
  // never structure, per svelte-adapter-dom-shim skill §15), so rather than building a
@@ -24,41 +22,39 @@
24
22
  <script lang="ts">
25
23
  import {
26
24
  createInitialModalState,
25
+ isModalVisible,
27
26
  modalReducer,
27
+ modalVisibilityAction,
28
28
  renderModal,
29
29
  resolveAccessibilityProps,
30
30
  shouldRenderModal,
31
31
  } from '@symbiote-native/components';
32
- import { dlog } from '@symbiote-native/engine';
32
+ import { dlog, Platform } from '@symbiote-native/engine';
33
33
  import { createAttachmentsSync } from '../../runes/attachments';
34
34
  import type { ShimElement } from '../../dom-shim';
35
35
 
36
36
  let rawProps: IModalProps = $props();
37
37
 
38
- let state = $state(createInitialModalState(rawProps.visible === true));
38
+ let state = $state(createInitialModalState(isModalVisible(rawProps.visible)));
39
39
 
40
40
  const resolved = $derived(resolveAccessibilityProps(rawProps));
41
- const isVisible = $derived(resolved.visible === true);
41
+ const isVisible = $derived(isModalVisible(resolved.visible));
42
42
  const shouldRender = $derived(shouldRenderModal(isVisible, state));
43
43
 
44
- // POST-render, mirroring state/modal.ts's contract: fires after the DOM update following the
45
- // render that used the OLD state, so a visible->hidden transition keeps the node mounted one
46
- // more frame (state.isRendered still true) before the NEXT render drops it. The reducer is
47
- // identity-stable, so a no-op transition — including this effect's own first run on mount —
48
- // triggers no extra render.
49
- //
50
- // Under this adapter's microtask-coalesced requestCommit() (element.ts's `p` setter), Svelte
51
- // settles the whole visible->hidden cascade within the SAME microtask flush that processes the
52
- // triggering prop change, so the keep-alive frame genuinely happens (a render sees
53
- // isVisible=false with isRendered still true before this effect flips it) but isn't observable
54
- // as its own committed Fabric frame the way React's per-render synchronous commit makes it.
44
+ // Arms the iOS keep-alive on show; a hide is left to the native dismiss (state/modal.ts). The
45
+ // reducer is identity-stable, so the mount run triggers no extra render.
55
46
  $effect(() => {
56
- state = modalReducer(
57
- state,
58
- isVisible ? { type: 'show' } : { type: 'hide' },
59
- );
47
+ const action = modalVisibilityAction(isVisible);
48
+ if (action !== undefined) state = modalReducer(state, action);
60
49
  });
61
50
 
51
+ // Modal.js: onDismiss is iOS-only — it drops the keep-alive, then tells the app.
52
+ function handleDismiss(): void {
53
+ if (Platform.OS !== 'ios') return;
54
+ state = modalReducer(state, { type: 'hide' });
55
+ rawProps.onDismiss?.();
56
+ }
57
+
62
58
  $effect(() => {
63
59
  if (!shouldRender) dlog('Modal hidden -> no node committed');
64
60
  });
@@ -82,8 +78,10 @@
82
78
  style,
83
79
  class: className,
84
80
  children: _children,
85
- ...passthrough
81
+ onDismiss: _onDismiss,
82
+ ...rest
86
83
  } = resolved;
84
+ const passthrough = { ...rest, onDismiss: handleDismiss };
87
85
 
88
86
  const descriptor = renderModal({
89
87
  visible,
@@ -106,6 +106,8 @@
106
106
  scrollEventThrottle={props.scrollEventThrottle}
107
107
  keyboardShouldPersistTaps={props.keyboardShouldPersistTaps}
108
108
  keyboardDismissMode={props.keyboardDismissMode}
109
+ removeClippedSubviews={props.removeClippedSubviews}
110
+ nestedScrollEnabled={props.nestedScrollEnabled}
109
111
  style={props.style}
110
112
  contentContainerStyle={props.contentContainerStyle}
111
113
  class={props.class}
@@ -154,6 +154,8 @@
154
154
  scrollEventThrottle: props.scrollEventThrottle,
155
155
  keyboardShouldPersistTaps: props.keyboardShouldPersistTaps,
156
156
  keyboardDismissMode: props.keyboardDismissMode,
157
+ removeClippedSubviews: props.removeClippedSubviews,
158
+ nestedScrollEnabled: props.nestedScrollEnabled,
157
159
  style: props.style,
158
160
  contentContainerStyle: props.contentContainerStyle,
159
161
  class: props.class,
@@ -437,11 +439,12 @@
437
439
  };
438
440
  });
439
441
 
442
+ // VirtualizedList.js: `[inversionStyle, style]` — the app's style can override the flip.
440
443
  const resolvedStyle = $derived(
441
444
  narrowed.inverted
442
445
  ? [
443
- narrowed.style,
444
446
  narrowed.horizontal ? INVERTED_X_STYLE : INVERTED_Y_STYLE,
447
+ narrowed.style,
445
448
  ]
446
449
  : narrowed.style,
447
450
  );
@@ -482,6 +485,13 @@
482
485
  }
483
486
  if (narrowed.keyboardDismissMode !== undefined)
484
487
  bag.keyboardDismissMode = narrowed.keyboardDismissMode;
488
+ // RN's VirtualizedList spreads its props onto the ScrollView, this one included.
489
+ if (narrowed.removeClippedSubviews !== undefined)
490
+ bag.removeClippedSubviews = narrowed.removeClippedSubviews;
491
+ if (narrowed.nestedScrollEnabled !== undefined)
492
+ bag.nestedScrollEnabled = narrowed.nestedScrollEnabled;
493
+ // VirtualizedList.js:1111 — Android moves the scrollbar back after the `scale: -1` flip.
494
+ if (narrowed.inverted === true) bag.isInvertedVirtualizedList = true;
485
495
  // `stickyHeaderIndices` is deliberately NOT forwarded. The behavior honours it by numbering the
486
496
  // owner's own PAINT children, and this list's indices are into the DATA stream — a windowed
487
497
  // list paints a spacer, a header and a slice, so index 3 of the data is almost never paint
@@ -62,6 +62,8 @@ export interface IVirtualizedListProps<ItemT> extends IAccessibilityProps, IAria
62
62
  scrollEventThrottle?: number;
63
63
  keyboardShouldPersistTaps?: boolean | 'always' | 'never' | 'handled';
64
64
  keyboardDismissMode?: 'none' | 'on-drag' | 'interactive';
65
+ removeClippedSubviews?: boolean;
66
+ nestedScrollEnabled?: boolean;
65
67
  style?: IStyleProp<IViewStyle>;
66
68
  contentContainerStyle?: IStyleProp<IViewStyle>;
67
69
  class?: ISvelteClassValue;
@@ -247,6 +247,8 @@
247
247
  scrollEventThrottle={props.scrollEventThrottle}
248
248
  keyboardShouldPersistTaps={props.keyboardShouldPersistTaps}
249
249
  keyboardDismissMode={props.keyboardDismissMode}
250
+ removeClippedSubviews={props.removeClippedSubviews}
251
+ nestedScrollEnabled={props.nestedScrollEnabled}
250
252
  style={props.style}
251
253
  contentContainerStyle={props.contentContainerStyle}
252
254
  class={props.class}
@@ -61,6 +61,8 @@ export interface IVirtualizedSectionListProps<ItemT> extends IAccessibilityProps
61
61
  scrollEventThrottle?: number;
62
62
  keyboardShouldPersistTaps?: boolean | 'always' | 'never' | 'handled';
63
63
  keyboardDismissMode?: 'none' | 'on-drag' | 'interactive';
64
+ removeClippedSubviews?: boolean;
65
+ nestedScrollEnabled?: boolean;
64
66
  style?: IStyleProp<IViewStyle>;
65
67
  contentContainerStyle?: IStyleProp<IViewStyle>;
66
68
  class?: ISvelteClassValue;
@@ -1,2 +1,3 @@
1
1
  export declare const CANONICAL_PROP_NAMES: readonly string[];
2
2
  export declare const CANONICAL_BY_LOWER: ReadonlyMap<string, string>;
3
+ export declare const BOOLEAN_PROP_NAMES: ReadonlySet<string>;
@@ -321,3 +321,87 @@ export const CANONICAL_PROP_NAMES = [
321
321
  // collide: the derivation asserts it (`canonical-prop-names.test.ts`), because two props whose
322
322
  // names differ only in case would make the repair a coin toss.
323
323
  export const CANONICAL_BY_LOWER = new Map(CANONICAL_PROP_NAMES.map(name => [name.toLowerCase(), name]));
324
+ // The subset whose type takes a boolean and no string anywhere: for these "" can only be the HTML
325
+ // shorthand for true. Svelte puts a static attribute on a hyphen-less tag (`<view accessible>`)
326
+ // into the template as `accessible=""`, and Android's ViewManager throws on a String where it
327
+ // casts to Boolean. Same derivation, same guard (`canonical-prop-names.test.ts`).
328
+ export const BOOLEAN_PROP_NAMES = new Set([
329
+ 'accessibilityElementsHidden',
330
+ 'accessibilityIgnoresInvertColors',
331
+ 'accessibilityRespondsToUserInteraction',
332
+ 'accessibilityShowsLargeContentViewer',
333
+ 'accessibilityViewIsModal',
334
+ 'accessible',
335
+ 'adjustsFontSizeToFit',
336
+ 'allowFontScaling',
337
+ 'allowSwipeDismissal',
338
+ 'alwaysBounceHorizontal',
339
+ 'alwaysBounceVertical',
340
+ 'android_disableSound',
341
+ 'animated',
342
+ 'animating',
343
+ 'aria-busy',
344
+ 'aria-checked',
345
+ 'aria-disabled',
346
+ 'aria-expanded',
347
+ 'aria-hidden',
348
+ 'aria-modal',
349
+ 'aria-selected',
350
+ 'autoCorrect',
351
+ 'autoFocus',
352
+ 'automaticallyAdjustKeyboardInsets',
353
+ 'blockNativeResponder',
354
+ 'blurOnSubmit',
355
+ 'bounces',
356
+ 'bouncesZoom',
357
+ 'cancelable',
358
+ 'centerContent',
359
+ 'collapsable',
360
+ 'directionalLockEnabled',
361
+ 'disableIntervalMomentum',
362
+ 'disabled',
363
+ 'editable',
364
+ 'enabled',
365
+ 'focusable',
366
+ 'hardwareAccelerated',
367
+ 'hasTVPreferredFocus',
368
+ 'hidden',
369
+ 'hidesWhenStopped',
370
+ 'highlighted',
371
+ 'horizontal',
372
+ 'invertStickyHeaders',
373
+ 'inverted',
374
+ 'keyboardShouldPersistTaps',
375
+ 'multiline',
376
+ 'navigationBarTranslucent',
377
+ 'needsOffscreenAlphaCompositing',
378
+ 'nestedScrollEnabled',
379
+ 'networkActivityIndicatorVisible',
380
+ 'pagingEnabled',
381
+ 'persistentScrollbar',
382
+ 'pinchGestureEnabled',
383
+ 'progressiveRenderingEnabled',
384
+ 'readOnly',
385
+ 'refreshing',
386
+ 'removeClippedSubviews',
387
+ 'renderToHardwareTextureAndroid',
388
+ 'screenReaderFocusable',
389
+ 'scrollEnabled',
390
+ 'secureTextEntry',
391
+ 'selectTextOnFocus',
392
+ 'selectable',
393
+ 'shouldRasterizeIOS',
394
+ 'showSoftInputOnFocus',
395
+ 'showsHorizontalScrollIndicator',
396
+ 'showsVerticalScrollIndicator',
397
+ 'snapToEnd',
398
+ 'snapToStart',
399
+ 'statusBarTranslucent',
400
+ 'stickySectionHeadersEnabled',
401
+ 'testOnly_pressed',
402
+ 'touchSoundDisabled',
403
+ 'translucent',
404
+ 'transparent',
405
+ 'useForeground',
406
+ 'visible',
407
+ ]);
@@ -25,7 +25,7 @@
25
25
  import { createElement, dlog, routeProp, toPublicInstance, } from '@symbiote-native/engine';
26
26
  import { descriptorFor } from '@symbiote-native/components';
27
27
  import { normalizeSvelteClass } from '../class-value.js';
28
- import { CANONICAL_BY_LOWER, CANONICAL_PROP_NAMES, } from './canonical-prop-names.js';
28
+ import { BOOLEAN_PROP_NAMES, CANONICAL_BY_LOWER, CANONICAL_PROP_NAMES, } from './canonical-prop-names.js';
29
29
  import { ShimNode } from './shim-node.js';
30
30
  import { discoverStyleCacheKey } from './style-cache.js';
31
31
  // The bag a not-yet-live element is diffed against in onMadeLive. A module constant rather than a
@@ -144,7 +144,9 @@ export class ShimElement extends ShimElementBase {
144
144
  // one we do not know) is handed on untouched. See `canonical-prop-names.ts`.
145
145
  setAttribute(name, value) {
146
146
  (this.attributes ??= new Map()).set(name, value);
147
- this.writeBagKey(CANONICAL_BY_LOWER.get(name) ?? name, value);
147
+ const canonical = CANONICAL_BY_LOWER.get(name) ?? name;
148
+ // `<view accessible>` arrives as "" — the HTML shorthand for true on a boolean prop.
149
+ this.writeBagKey(canonical, value === '' && BOOLEAN_PROP_NAMES.has(canonical) ? true : value);
148
150
  }
149
151
  getAttribute(name) {
150
152
  return this.attributes?.get(name) ?? null;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@symbiote-native/svelte",
3
- "version": "3.0.0",
3
+ "version": "3.0.2",
4
4
  "description": "SymbioteNative's Svelte adapter — a DOM shim over stock compiled Svelte output driving real native iOS/Android views through the same engine as every other adapter.",
5
5
  "keywords": [
6
6
  "react-native",
@@ -73,22 +73,22 @@
73
73
  },
74
74
  "dependencies": {
75
75
  "typescript": "~6.0.0",
76
- "@symbiote-native/components": "3.0.0",
76
+ "@symbiote-native/components": "3.1.0",
77
77
  "@symbiote-native/css-parser": "0.5.0"
78
78
  },
79
79
  "peerDependencies": {
80
80
  "svelte": ">=5.56.0",
81
81
  "react-native": ">=0.86",
82
- "@symbiote-native/engine": "^1.0.0"
82
+ "@symbiote-native/engine": "^1.3.0"
83
83
  },
84
84
  "devDependencies": {
85
85
  "@vue/runtime-core": "^3.5.13",
86
86
  "react": "19.2.3",
87
87
  "svelte": "^5.56.0",
88
- "@symbiote-native/engine": "1.0.0",
89
- "@symbiote-native/react": "3.0.0",
90
- "@symbiote-native/test-utils": "0.3.1",
91
- "@symbiote-native/vue": "3.0.0"
88
+ "@symbiote-native/engine": "1.3.0",
89
+ "@symbiote-native/test-utils": "0.4.2",
90
+ "@symbiote-native/react": "3.0.2",
91
+ "@symbiote-native/vue": "3.0.2"
92
92
  },
93
93
  "scripts": {
94
94
  "typecheck": "tsc --build",