@symbiote-native/react 3.2.0 → 3.4.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.
Files changed (84) hide show
  1. package/build/batched-updates.d.ts +2 -0
  2. package/build/batched-updates.js +5 -0
  3. package/build/components/checkbox-props.d.ts +5 -0
  4. package/build/components/checkbox-props.js +1 -0
  5. package/build/components/flat-list/index.d.ts +16 -13
  6. package/build/components/flat-list/index.js +86 -72
  7. package/build/components/input-accessory-view-props.d.ts +4 -3
  8. package/build/components/keyboard-avoiding-view/index.d.ts +2 -2
  9. package/build/components/keyboard-avoiding-view/index.js +45 -84
  10. package/build/components/layout-conformance-props.d.ts +6 -0
  11. package/build/components/layout-conformance-props.js +1 -0
  12. package/build/components/modal/index.d.ts +4 -4
  13. package/build/components/modal/index.js +47 -18
  14. package/build/components/refresh-control-props.d.ts +7 -6
  15. package/build/components/scroll-view/scroll-view-props.d.ts +8 -5
  16. package/build/components/text-input/text-input-props.d.ts +4 -0
  17. package/build/components/touchable/touchable-props.d.ts +6 -5
  18. package/build/components/virtualized-list/index.d.ts +3 -70
  19. package/build/components/virtualized-list/index.js +50 -548
  20. package/build/components/virtualized-list/list-children.d.ts +13 -0
  21. package/build/components/virtualized-list/list-children.js +79 -0
  22. package/build/components/virtualized-list/list-config.d.ts +11 -0
  23. package/build/components/virtualized-list/list-config.js +14 -0
  24. package/build/components/virtualized-list/list-elements.d.ts +11 -0
  25. package/build/components/virtualized-list/list-elements.js +26 -0
  26. package/build/components/virtualized-list/list-host.d.ts +14 -0
  27. package/build/components/virtualized-list/list-host.js +24 -0
  28. package/build/components/virtualized-list/list-scroll-props.d.ts +32 -0
  29. package/build/components/virtualized-list/list-scroll-props.js +30 -0
  30. package/build/components/virtualized-list/nested-scope.d.ts +10 -0
  31. package/build/components/virtualized-list/nested-scope.js +16 -0
  32. package/build/components/virtualized-list/use-list-driver.d.ts +22 -0
  33. package/build/components/virtualized-list/use-list-driver.js +104 -0
  34. package/build/components/virtualized-list/use-list-nesting.d.ts +12 -0
  35. package/build/components/virtualized-list/use-list-nesting.js +37 -0
  36. package/build/components/virtualized-list/use-list-parts.d.ts +12 -0
  37. package/build/components/virtualized-list/use-list-parts.js +20 -0
  38. package/build/components/virtualized-list/use-scroll-handlers.d.ts +17 -0
  39. package/build/components/virtualized-list/use-scroll-handlers.js +106 -0
  40. package/build/components/virtualized-list/use-separators.d.ts +8 -0
  41. package/build/components/virtualized-list/use-separators.js +17 -0
  42. package/build/components/virtualized-list/virtualized-list-props.d.ts +85 -0
  43. package/build/components/virtualized-list/virtualized-list-props.js +2 -0
  44. package/build/components/virtualized-section-list/index.d.ts +24 -8
  45. package/build/components/virtualized-section-list/index.js +89 -93
  46. package/build/components/virtualized-section-list/section-item-cell.d.ts +23 -0
  47. package/build/components/virtualized-section-list/section-item-cell.js +48 -0
  48. package/build/components.d.ts +5 -5
  49. package/build/descriptor-to-react/with-children.d.ts +4 -0
  50. package/build/descriptor-to-react/with-children.js +6 -0
  51. package/build/hooks/use-animated-value.d.ts +7 -0
  52. package/build/hooks/use-animated-value.js +19 -0
  53. package/build/hooks/use-color-scheme.d.ts +2 -2
  54. package/build/hooks/use-color-scheme.js +4 -13
  55. package/build/hooks/use-event.d.ts +6 -0
  56. package/build/hooks/use-event.js +15 -0
  57. package/build/hooks/use-native-view-controller.d.ts +3 -0
  58. package/build/hooks/use-native-view-controller.js +15 -0
  59. package/build/hooks/use-pressability.d.ts +34 -0
  60. package/build/hooks/use-pressability.js +39 -0
  61. package/build/hooks/use-window-dimensions.js +6 -10
  62. package/build/host-config.js +6 -5
  63. package/build/index.d.ts +23 -5
  64. package/build/index.js +23 -14
  65. package/build/jsx-runtime.d.ts +17 -16
  66. package/build/modules/animated/create-animated-component.d.ts +2 -2
  67. package/build/modules/animated/create-animated-component.js +10 -22
  68. package/build/modules/animated/index.d.ts +54 -41
  69. package/build/modules/animated/index.js +2 -27
  70. package/build/modules/app-registry/index.d.ts +3 -3
  71. package/build/modules/app-registry/index.js +2 -2
  72. package/build/modules/status-bar/index.android.d.ts +1 -1
  73. package/build/modules/status-bar/index.android.js +7 -27
  74. package/build/modules/status-bar/index.ios.d.ts +1 -1
  75. package/build/modules/status-bar/index.ios.js +4 -28
  76. package/build/modules/status-bar/shared.d.ts +5 -4
  77. package/build/modules/status-bar/shared.js +33 -6
  78. package/build/native-host-tags.d.ts +2 -0
  79. package/build/native-host-tags.js +4 -0
  80. package/build/render.d.ts +1 -1
  81. package/build/render.js +3 -1
  82. package/build/root-tag-context.d.ts +1 -0
  83. package/build/root-tag-context.js +4 -0
  84. package/package.json +6 -6
@@ -0,0 +1,2 @@
1
+ export declare function unstable_batchedUpdates<A, R>(callback: (a: A) => R, a: A): R;
2
+ export declare function unstable_batchedUpdates<R>(callback: () => R): R;
@@ -0,0 +1,5 @@
1
+ // RN's `unstable_batchedUpdates`, over this adapter's own reconciler instead of RN's renderer
2
+ import reconciler from './host-config.js';
3
+ export function unstable_batchedUpdates(callback, a) {
4
+ return reconciler.batchedUpdates(callback, a);
5
+ }
@@ -0,0 +1,5 @@
1
+ import type { ICheckboxProps as ICheckboxBaseProps } from '@symbiote-native/components';
2
+ import type { IViewProps } from '../components';
3
+ export type ICheckboxProps = ICheckboxBaseProps & Omit<IViewProps, keyof ICheckboxBaseProps | 'children'> & {
4
+ className?: string;
5
+ };
@@ -0,0 +1 @@
1
+ export {};
@@ -1,17 +1,13 @@
1
- import { type ComponentType, type ReactElement, type ReactNode, type Ref } from 'react';
1
+ import { type ComponentType, type ReactElement, type Ref } from 'react';
2
2
  import { type ISymbioteEvent } from '@symbiote-native/engine';
3
- import { type ISeparators, type ISeparatorProps, type IViewabilityConfig, type IViewabilityConfigCallbackPair, type IViewableItemsChangedInfo, type IVirtualizedListHandle } from '../virtualized-list';
4
- import type { IAccessibilityProps, IAriaProps } from '@symbiote-native/components';
3
+ import { type ICellRendererComponent, type IListItemInfo, type IRenderItem, type ISeparatorComponent, type IViewabilityConfig, type IViewabilityConfigCallbackPair, type IViewableItemsChangedInfo, type IVirtualizedListHandle } from '../virtualized-list';
4
+ import type { IAccessibilityProps, IAriaProps, IInnerViewRef } from '@symbiote-native/components';
5
5
  import type { IStyleProp, IViewStyle } from '../../utils/styles';
6
- type IRenderItem<ItemT> = (info: {
7
- item: ItemT;
8
- index: number;
9
- separators: ISeparators;
10
- }) => ReactNode;
11
6
  export type IFlatListHandle = IVirtualizedListHandle;
12
- export interface IFlatListProps<ItemT> extends IAccessibilityProps, IAriaProps {
7
+ export type IFlatListProps<ItemT> = IAccessibilityProps & IAriaProps & {
13
8
  data: readonly ItemT[];
14
- renderItem: IRenderItem<ItemT>;
9
+ renderItem?: IRenderItem<ItemT>;
10
+ ListItemComponent?: ComponentType<IListItemInfo<ItemT>>;
15
11
  keyExtractor?: (item: ItemT, index: number) => string;
16
12
  getItemLayout?: (data: unknown, index: number) => {
17
13
  length: number;
@@ -20,10 +16,13 @@ export interface IFlatListProps<ItemT> extends IAccessibilityProps, IAriaProps {
20
16
  };
21
17
  numColumns?: number;
22
18
  columnWrapperStyle?: IStyleProp<IViewStyle> | string;
23
- ItemSeparatorComponent?: ComponentType<ISeparatorProps<ItemT>>;
19
+ ItemSeparatorComponent?: ISeparatorComponent<ItemT>;
20
+ CellRendererComponent?: ICellRendererComponent<unknown>;
24
21
  ListHeaderComponent?: ComponentType<Record<string, never>> | ReactElement;
25
22
  ListFooterComponent?: ComponentType<Record<string, never>> | ReactElement;
26
23
  ListEmptyComponent?: ComponentType<Record<string, never>> | ReactElement;
24
+ ListHeaderComponentStyle?: IStyleProp<IViewStyle>;
25
+ ListFooterComponentStyle?: IStyleProp<IViewStyle>;
27
26
  horizontal?: boolean;
28
27
  inverted?: boolean;
29
28
  extraData?: unknown;
@@ -51,6 +50,8 @@ export interface IFlatListProps<ItemT> extends IAccessibilityProps, IAriaProps {
51
50
  maxToRenderPerBatch?: number;
52
51
  updateCellsBatchingPeriod?: number;
53
52
  windowSize?: number;
53
+ disableVirtualization?: boolean;
54
+ stickyHeaderIndices?: number[];
54
55
  maintainVisibleContentPosition?: {
55
56
  minIndexForVisible: number;
56
57
  autoscrollToTopThreshold?: number;
@@ -60,6 +61,7 @@ export interface IFlatListProps<ItemT> extends IAccessibilityProps, IAriaProps {
60
61
  onScrollEndDrag?: (event: ISymbioteEvent) => void;
61
62
  onMomentumScrollBegin?: (event: ISymbioteEvent) => void;
62
63
  onMomentumScrollEnd?: (event: ISymbioteEvent) => void;
64
+ onContentSizeChange?: (width: number, height: number) => void;
63
65
  scrollEventThrottle?: number;
64
66
  keyboardShouldPersistTaps?: boolean | 'always' | 'never' | 'handled';
65
67
  keyboardDismissMode?: 'none' | 'on-drag' | 'interactive';
@@ -68,8 +70,9 @@ export interface IFlatListProps<ItemT> extends IAccessibilityProps, IAriaProps {
68
70
  className?: string;
69
71
  removeClippedSubviews?: boolean;
70
72
  nestedScrollEnabled?: boolean;
71
- }
73
+ stickyHeaderHiddenOnScroll?: boolean;
74
+ innerViewRef?: IInnerViewRef;
75
+ };
72
76
  export declare function FlatList<ItemT>(props: IFlatListProps<ItemT> & {
73
77
  ref?: Ref<IFlatListHandle>;
74
78
  }): ReactElement;
75
- export {};
@@ -5,90 +5,104 @@
5
5
  // scrolling are inherited from VirtualizedList; the data shaping and the row/viewability/separator
6
6
  // transforms are shared from @symbiote-native/components. This file only adapts to React's lifecycle
7
7
  // (element creation + ref threading).
8
- import { createElement, } from 'react';
8
+ import { createElement, isValidElement, } from 'react';
9
9
  import { Platform, dlog, resolveClassName, } from '@symbiote-native/engine';
10
- import { SINGLE_COLUMN, chunkIntoRows, expandRowViewability, firstItemOfRow, lastItemOfRow, removeClippedSubviewsOrDefault, rowKeyExtractor, } from '@symbiote-native/components';
10
+ import { SINGLE_COLUMN, arrayLikeLength, chunkIntoRows, expandRowToken, expandRowViewability, firstItemOfRow, lastItemOfRow, removeClippedSubviewsOrDefault, rowKeyExtractor, } from '@symbiote-native/components';
11
11
  import { VirtualizedList, } from '../virtualized-list/index.js';
12
- export function FlatList(props) {
13
- const { ref, data, renderItem, keyExtractor, numColumns = SINGLE_COLUMN, columnWrapperStyle,
14
- // onViewableItemsChanged/viewability are typed against ItemT here; in the multi-column path
15
- // the underlying stream is IRow<ItemT>, so they are dropped from `rest` and pulled out.
16
- onViewableItemsChanged, viewabilityConfigCallbackPairs,
17
- // ItemSeparatorComponent is typed on ItemT; the multi-column stream is IRow<ItemT>, so it is
18
- // wrapped there to unwrap rows back to items, exactly like viewability above.
19
- ItemSeparatorComponent, removeClippedSubviews: authoredRemoveClippedSubviews, ...ownRest } = props;
20
- // FlatList.js always sends it, defaulted per platform.
21
- const rest = {
22
- ...ownRest,
23
- removeClippedSubviews: removeClippedSubviewsOrDefault(authoredRemoveClippedSubviews, Platform.OS),
24
- };
25
- dlog(`FlatList over ${data.length} items, ${numColumns} column(s)`);
26
- if (numColumns <= SINGLE_COLUMN) {
27
- return createElement((VirtualizedList), {
28
- ref,
29
- data,
30
- getItem: (_source, index) => data[index],
31
- getItemCount: () => data.length,
32
- renderItem,
33
- keyExtractor,
34
- onViewableItemsChanged,
35
- viewabilityConfigCallbackPairs,
36
- ItemSeparatorComponent,
37
- ...rest,
38
- });
39
- }
40
- // Multi-column: the virtualized stream is rows. Each cell renders its items side by side in a
41
- // flex-row View so windowing accounts for whole rows.
42
- const rows = chunkIntoRows(data, numColumns);
43
- const resolvedColumnWrapperStyle = typeof columnWrapperStyle === 'string'
44
- ? resolveClassName(columnWrapperStyle)
45
- : columnWrapperStyle;
12
+ import { renderItemElement } from '../virtualized-list/list-elements.js';
13
+ function singleColumnList(typed, rest, ref) {
14
+ const { data } = typed;
15
+ return createElement((VirtualizedList), {
16
+ ref,
17
+ getItem: (_source, index) => data[index],
18
+ getItemCount: () => arrayLikeLength(data),
19
+ renderItem: typed.renderItem,
20
+ ListItemComponent: typed.ListItemComponent,
21
+ keyExtractor: typed.keyExtractor,
22
+ onViewableItemsChanged: typed.onViewableItemsChanged,
23
+ viewabilityConfigCallbackPairs: typed.viewabilityConfigCallbackPairs,
24
+ ItemSeparatorComponent: typed.ItemSeparatorComponent,
25
+ data,
26
+ ...rest,
27
+ });
28
+ }
29
+ // The row is the virtualized cell, so its items share one `separators` handle, as in RN
30
+ function rowRenderer(typed) {
31
+ const { renderItem, ListItemComponent, keyExtractor, columnWrapperStyle } = typed;
46
32
  const rowStyle = [
47
33
  { flexDirection: 'row' },
48
- resolvedColumnWrapperStyle,
34
+ typeof columnWrapperStyle === 'string'
35
+ ? resolveClassName(columnWrapperStyle)
36
+ : columnWrapperStyle,
49
37
  ];
50
- const renderRow = (info) => {
51
- const cells = info.item.items.map((item, column) => {
52
- const index = info.item.startIndex + column;
53
- const key = keyExtractor ? keyExtractor(item, index) : String(index);
54
- // The row IS the virtualized cell, so every item in it shares the row's separators handle
55
- // (the divider sits between rows, not columns), like RN's multi-column FlatList.
56
- return createElement('view', { key, style: { flex: 1 } }, renderItem({ item, index, separators: info.separators }));
57
- });
38
+ return info => {
39
+ const cells = expandRowToken({ item: info.item, key: '', index: info.index, isViewable: true }, keyExtractor).map(({ item, index, key }) => createElement('view', { key, style: { flex: 1 } }, renderItemElement({ renderItem, ListItemComponent }, { item, index, separators: info.separators })));
58
40
  return createElement('view', { style: rowStyle }, ...cells);
59
41
  };
60
- // Viewability over rows expands back to per-item tokens so the caller sees item-level
61
- // visibility, not row-level (shared expandRowViewability).
62
- const rowOnViewableItemsChanged = onViewableItemsChanged !== undefined
63
- ? (rowInfo) => {
64
- onViewableItemsChanged(expandRowViewability(rowInfo, keyExtractor));
65
- }
66
- : undefined;
67
- const rowViewabilityPairs = viewabilityConfigCallbackPairs?.map(pair => ({
68
- viewabilityConfig: pair.viewabilityConfig,
69
- onViewableItemsChanged: (rowInfo) => {
70
- pair.onViewableItemsChanged(expandRowViewability(rowInfo, keyExtractor));
71
- },
72
- }));
73
- // The divider between rows shows real items (last of the row above, first of the row below), so
74
- // the user's separator, typed on ItemT, sees items rather than the IRow wrapper.
75
- const rowSeparatorComponent = ItemSeparatorComponent === undefined
76
- ? undefined
77
- : (rowProps) => createElement(ItemSeparatorComponent, {
78
- ...rowProps,
79
- leadingItem: lastItemOfRow(rowProps.leadingItem),
80
- trailingItem: firstItemOfRow(rowProps.trailingItem),
81
- });
42
+ }
43
+ // Row reports expand back to per-item tokens through `expandRowViewability`
44
+ function rowViewability(typed) {
45
+ const { keyExtractor, onViewableItemsChanged } = typed;
46
+ const expand = (report) => (rowInfo) => {
47
+ report(expandRowViewability(rowInfo, keyExtractor));
48
+ };
49
+ return {
50
+ onViewableItemsChanged: onViewableItemsChanged === undefined
51
+ ? undefined
52
+ : expand(onViewableItemsChanged),
53
+ pairs: typed.viewabilityConfigCallbackPairs?.map(pair => ({
54
+ viewabilityConfig: pair.viewabilityConfig,
55
+ onViewableItemsChanged: expand(pair.onViewableItemsChanged),
56
+ })),
57
+ };
58
+ }
59
+ // The divider between rows shows the last item above and the first below, not the `IRow`
60
+ function rowSeparator(separator) {
61
+ if (separator === undefined || isValidElement(separator))
62
+ return separator;
63
+ return (rowProps) => createElement(separator, {
64
+ ...rowProps,
65
+ leadingItem: lastItemOfRow(rowProps.leadingItem),
66
+ trailingItem: firstItemOfRow(rowProps.trailingItem),
67
+ });
68
+ }
69
+ // The virtualized stream is rows, each cell lays its items out side by side in a flex row
70
+ function multiColumnList(typed, numColumns, rest, ref) {
71
+ const rows = chunkIntoRows(typed.data, numColumns);
72
+ const viewability = rowViewability(typed);
82
73
  return createElement((VirtualizedList), {
83
74
  ref,
84
75
  data: rows,
85
76
  getItem: (_source, index) => rows[index],
86
77
  getItemCount: () => rows.length,
87
- renderItem: renderRow,
88
- keyExtractor: (row) => rowKeyExtractor(row, keyExtractor),
89
- onViewableItemsChanged: rowOnViewableItemsChanged,
90
- viewabilityConfigCallbackPairs: rowViewabilityPairs,
91
- ItemSeparatorComponent: rowSeparatorComponent,
78
+ renderItem: rowRenderer(typed),
79
+ keyExtractor: (row) => rowKeyExtractor(row, typed.keyExtractor),
80
+ onViewableItemsChanged: viewability.onViewableItemsChanged,
81
+ viewabilityConfigCallbackPairs: viewability.pairs,
82
+ ItemSeparatorComponent: rowSeparator(typed.ItemSeparatorComponent),
92
83
  ...rest,
93
84
  });
94
85
  }
86
+ export function FlatList(props) {
87
+ const { ref, data, renderItem, ListItemComponent, keyExtractor, numColumns = SINGLE_COLUMN, columnWrapperStyle, onViewableItemsChanged, viewabilityConfigCallbackPairs, ItemSeparatorComponent, removeClippedSubviews: authoredRemoveClippedSubviews, ...ownRest } = props;
88
+ // `FlatList.js` always sends it, defaulted per platform
89
+ const rest = {
90
+ ...ownRest,
91
+ removeClippedSubviews: removeClippedSubviewsOrDefault(authoredRemoveClippedSubviews, Platform.OS),
92
+ };
93
+ const typed = {
94
+ data,
95
+ renderItem,
96
+ ListItemComponent,
97
+ keyExtractor,
98
+ numColumns,
99
+ columnWrapperStyle,
100
+ onViewableItemsChanged,
101
+ viewabilityConfigCallbackPairs,
102
+ ItemSeparatorComponent,
103
+ };
104
+ dlog(`FlatList over ${arrayLikeLength(data)} items, ${numColumns} column(s)`);
105
+ return numColumns <= SINGLE_COLUMN
106
+ ? singleColumnList(typed, rest, ref)
107
+ : multiColumnList(typed, numColumns, rest, ref);
108
+ }
@@ -1,10 +1,11 @@
1
1
  import type { ReactNode } from 'react';
2
2
  import type { IAccessibilityProps, IAriaProps } from '@symbiote-native/components';
3
+ import type { IColorValue } from '@symbiote-native/engine';
3
4
  import type { IStyleProp, IViewStyle } from '../utils/styles';
4
- export interface IInputAccessoryViewProps extends IAccessibilityProps, IAriaProps {
5
+ export type IInputAccessoryViewProps = IAccessibilityProps & IAriaProps & {
5
6
  nativeID?: string;
6
- backgroundColor?: string;
7
+ backgroundColor?: IColorValue;
7
8
  style?: IStyleProp<IViewStyle>;
8
9
  className?: string;
9
10
  children?: ReactNode;
10
- }
11
+ };
@@ -4,7 +4,7 @@ import { type IKeyboardAvoidingBehavior } from '@symbiote-native/components';
4
4
  import type { IAccessibilityProps, IAriaProps } from '@symbiote-native/components';
5
5
  import type { IStyleProp, IViewStyle } from '../../utils/styles';
6
6
  export type { IKeyboardAvoidingBehavior } from '@symbiote-native/components';
7
- export interface IKeyboardAvoidingViewProps extends IAccessibilityProps, IAriaProps {
7
+ export type IKeyboardAvoidingViewProps = IAccessibilityProps & IAriaProps & {
8
8
  behavior?: IKeyboardAvoidingBehavior;
9
9
  enabled?: boolean;
10
10
  keyboardVerticalOffset?: number;
@@ -13,5 +13,5 @@ export interface IKeyboardAvoidingViewProps extends IAccessibilityProps, IAriaPr
13
13
  className?: string;
14
14
  children?: ReactNode;
15
15
  onLayout?: (event: ISymbioteEvent) => void;
16
- }
16
+ };
17
17
  export declare const KeyboardAvoidingView: FC<IKeyboardAvoidingViewProps>;
@@ -1,110 +1,71 @@
1
- // KeyboardAvoidingView: composes the host View and shifts it out of the keyboard's way as
2
- // the keyboard shows/hides. It subscribes to the Keyboard module (native->JS events) and
3
- // recomputes a bottom inset from the keyboard frame and the view's own measured frame.
4
- // Mirrors RN's Libraries/Components/Keyboard/KeyboardAvoidingView.js, as a function component.
5
- //
6
- // The inset math + the behavior → style/structure decision are framework-agnostic and live in
7
- // @symbiote-native/components (render-keyboard-avoiding-view), shared verbatim with the Vue adapter.
8
- // React supplies only the lifecycle: useState for the inset, useRef for the measured frame and
9
- // the cross-fade setting, the useEffect subscriptions, and the descriptor-free element assembly
10
- // around its children.
1
+ // KeyboardAvoidingView over the shared `createKeyboardAvoidingModel`, which holds the frame, the
2
+ // last keyboard event and the inset rules of RN's class
3
+ // React supplies the inset state, the keyboard subscriptions and the element assembly
11
4
  import { createElement, useEffect, useRef, useState, } from 'react';
12
- import { dlog, Keyboard, Platform, } from '@symbiote-native/engine';
13
- import { computeInset, configureKeyboardAvoidingAnimation, keyboardAvoidingEventNamesFor, readKeyboardAnimationTiming, readKeyboardFrame, readLayoutFrame, readPrefersCrossFadeTransitions, resolveKeyboardAvoidingLayout, DEFAULT_VERTICAL_OFFSET, } from '@symbiote-native/components';
5
+ import { Keyboard, Platform, } from '@symbiote-native/engine';
6
+ import { createKeyboardAvoidingModel, keyboardAvoidingEventNamesFor, readPrefersCrossFadeTransitions, resolveKeyboardAvoidingLayout, DEFAULT_VERTICAL_OFFSET, } from '@symbiote-native/components';
14
7
  import {} from '../../components.js';
15
- // The two notifications to subscribe to, per host (iOS takes the `will` pair so the view rides
16
- // up with the keyboard animation; Android has no will-notifications). Resolved ONCE at module
17
- // scope: Platform.OS is fixed for the process, so recomputing it per render buys nothing.
8
+ // Resolved once: Platform.OS is fixed for the process
18
9
  const KEYBOARD_EVENTS = keyboardAvoidingEventNamesFor(Platform.OS);
19
- export const KeyboardAvoidingView = props => {
20
- const { behavior, enabled = true, keyboardVerticalOffset = DEFAULT_VERTICAL_OFFSET, contentContainerStyle, style, children, onLayout,
21
- // The wrapper is the View FC, which runs resolveAccessibilityProps itself, so
22
- // the raw aria/role + accessibility* props pass through untouched here and fold
23
- // there once.
24
- ...accessibilityRest } = props;
10
+ // The model, its inset state and the keyboard subscriptions, kept apart so the component reads as
11
+ // the element assembly
12
+ function useKeyboardAvoidingModel(options) {
25
13
  const [inset, setInset] = useState(0);
26
- // Mutable, not state: changing the frame alone shouldn't re-render; it feeds the
27
- // next keyboard event's inset math.
28
- const frameRef = useRef(undefined);
29
- const initialHeightRef = useRef(undefined);
30
- // A device accessibility setting, not component state: it cannot change mid-session, and
31
- // learning it must not re-render. Read once per mount, then fed to every computeInset call.
14
+ // The model reads the options at event time, so a changed prop needs no new subscription
15
+ const optionsRef = useRef(options);
16
+ optionsRef.current = options;
17
+ // A device setting that cannot change mid-session, learning it must not re-render
32
18
  const prefersCrossFadeRef = useRef(false);
19
+ const [model] = useState(() => createKeyboardAvoidingModel({
20
+ options: () => optionsRef.current,
21
+ setInset,
22
+ prefersCrossFade: () => prefersCrossFadeRef.current,
23
+ }));
33
24
  useEffect(() => {
34
25
  void readPrefersCrossFadeTransitions().then(prefers => {
35
26
  prefersCrossFadeRef.current = prefers;
36
27
  });
37
28
  }, []);
38
29
  useEffect(() => {
39
- const onShow = (payload) => {
40
- const keyboard = readKeyboardFrame(payload);
41
- // Functional update, not a read of `inset`: this handler is built inside the effect, so a
42
- // direct read would freeze the value from the render that created it — and 'height' mode
43
- // feeds the LIVE inset back in to cancel the wrapper's own shrink.
44
- setInset(previousInset => {
45
- const next = computeInset(frameRef.current, keyboard, keyboardVerticalOffset, {
46
- behavior,
47
- previousInset,
48
- prefersCrossFadeTransitions: prefersCrossFadeRef.current,
49
- });
50
- // RN's `_updateBottomIfNecessary` skips the animation entirely when the inset did not
51
- // change (`if (this._bottom === height) return;`) — arming a transition for a value that
52
- // moves nowhere would only ever animate other props the app changed this frame.
53
- if (next !== previousInset)
54
- configureKeyboardAvoidingAnimation(readKeyboardAnimationTiming(payload), enabled);
55
- dlog(`KeyboardAvoidingView show -> inset ${next}`);
56
- return next;
57
- });
58
- };
59
- const onHide = () => {
60
- dlog('KeyboardAvoidingView hide -> inset 0');
61
- setInset(0);
62
- };
63
30
  const subscriptions = [
64
- Keyboard.addListener(KEYBOARD_EVENTS.show, onShow),
65
- Keyboard.addListener(KEYBOARD_EVENTS.hide, onHide),
31
+ Keyboard.addListener(KEYBOARD_EVENTS.show, model.keyboardShown),
32
+ Keyboard.addListener(KEYBOARD_EVENTS.hide, model.keyboardHidden),
66
33
  ];
67
34
  return () => {
68
35
  for (const subscription of subscriptions)
69
36
  subscription.remove();
70
37
  };
71
- }, [behavior, keyboardVerticalOffset, enabled]);
38
+ }, [model]);
39
+ return { inset, model };
40
+ }
41
+ // `onLayout` is widened through a typed variable (no `as`), View's public type lacks it
42
+ function wrapperElement(rest, wrapStyle, onLayout, content) {
43
+ const wrapperProps = { ...rest, style: wrapStyle, onLayout, children: content };
44
+ return createElement('view', wrapperProps);
45
+ }
46
+ export const KeyboardAvoidingView = props => {
47
+ const { behavior, enabled = true, keyboardVerticalOffset = DEFAULT_VERTICAL_OFFSET, contentContainerStyle, style, children, onLayout,
48
+ // The wrapper is the View FC, which folds aria/role and accessibility* once itself
49
+ ...accessibilityRest } = props;
50
+ const { inset, model } = useKeyboardAvoidingModel({
51
+ behavior,
52
+ enabled,
53
+ keyboardVerticalOffset,
54
+ });
72
55
  const handleLayout = (event) => {
73
- const frame = readLayoutFrame(event.nativeEvent.layout);
74
- if (frame !== undefined) {
75
- frameRef.current = frame;
76
- if (initialHeightRef.current === undefined)
77
- initialHeightRef.current = frame.height;
78
- }
56
+ model.laidOut(event.nativeEvent.layout);
79
57
  onLayout?.(event);
80
58
  };
81
- // When disabled the inset is forced to 0, so every behavior mode renders the view
82
- // untouched (RN gates each bottomHeight/height computation on `enabled ?? true`).
83
- const effectiveInset = enabled ? inset : 0;
59
+ // Disabled forces the inset to 0, so every behavior mode renders the view untouched
84
60
  const layout = resolveKeyboardAvoidingLayout({
85
61
  behavior,
86
- effectiveInset,
87
- initialHeight: initialHeightRef.current,
62
+ effectiveInset: enabled ? inset : 0,
63
+ initialHeight: model.initialHeight(),
88
64
  style,
89
65
  contentContainerStyle,
90
66
  });
91
- // 'nested' ('position') pushes the content in an inner View by `bottom: inset`; the wrapper
92
- // modes adjust the single wrapper directly.
93
- if (layout.kind === 'nested') {
94
- return renderWrapper(layout.wrapperStyle, createElement('view', { style: layout.innerStyle }, children));
95
- }
96
- return renderWrapper(layout.wrapperStyle, children);
97
- // The wrapper carries onLayout. The View FC's public props don't surface it, but
98
- // `view` routes the base layout event at runtime; widen the props through
99
- // a typed variable (no inline-literal excess-property check, no `as`) so the
100
- // onLayout reaches the host without editing View's public type.
101
- function renderWrapper(wrapStyle, content) {
102
- const wrapperProps = {
103
- ...accessibilityRest,
104
- style: wrapStyle,
105
- onLayout: handleLayout,
106
- children: content,
107
- };
108
- return createElement('view', wrapperProps);
109
- }
67
+ const content = layout.kind === 'nested'
68
+ ? createElement('view', { style: layout.innerStyle }, children)
69
+ : children;
70
+ return wrapperElement(accessibilityRest, layout.wrapperStyle, handleLayout, content);
110
71
  };
@@ -0,0 +1,6 @@
1
+ import type { ReactNode } from 'react';
2
+ import type { ILayoutConformanceMode } from '@symbiote-native/components';
3
+ export type ILayoutConformanceProps = {
4
+ mode: ILayoutConformanceMode;
5
+ children?: ReactNode;
6
+ };
@@ -0,0 +1 @@
1
+ export {};
@@ -1,12 +1,12 @@
1
1
  import { type FC, type ReactNode } from 'react';
2
- import { type ISymbioteEvent } from '@symbiote-native/engine';
2
+ import { type IColorValue, type ISymbioteEvent } from '@symbiote-native/engine';
3
3
  import { type IAccessibilityProps, type IAriaProps, type IModalAnimationType, type IModalOrientation, type IModalPresentationStyle } from '@symbiote-native/components';
4
4
  import type { IStyleProp, IViewStyle } from '../../utils/styles';
5
5
  export type { IModalAnimationType, IModalPresentationStyle, IModalOrientation, IModalOrientationChangeEvent, } from '@symbiote-native/components';
6
- export interface IModalProps extends IAccessibilityProps, IAriaProps {
6
+ export type IModalProps = IAccessibilityProps & IAriaProps & {
7
7
  visible?: boolean;
8
8
  transparent?: boolean;
9
- backdropColor?: string;
9
+ backdropColor?: IColorValue;
10
10
  animationType?: IModalAnimationType;
11
11
  presentationStyle?: IModalPresentationStyle;
12
12
  supportedOrientations?: ReadonlyArray<IModalOrientation>;
@@ -21,5 +21,5 @@ export interface IModalProps extends IAccessibilityProps, IAriaProps {
21
21
  style?: IStyleProp<IViewStyle>;
22
22
  className?: string;
23
23
  children?: ReactNode;
24
- }
24
+ };
25
25
  export declare const Modal: FC<IModalProps>;
@@ -12,17 +12,21 @@
12
12
  // through the exit transition. The native exit-animation timing is what's deferred, not the
13
13
  // callback contract.
14
14
  import { createElement, useEffect, useReducer, } from 'react';
15
- import { dlog, Platform } from '@symbiote-native/engine';
16
- import { createInitialModalState, isModalVisible, modalReducer, modalVisibilityAction, renderModal, resolveAccessibilityProps, shouldRenderModal, } from '@symbiote-native/components';
17
- export const Modal = rawProps => {
18
- // Modal owns its host element (modal), so it folds aria/role here; the resolved fields
19
- // ride the host node via `...passthrough`. The events (onShow/onDismiss/onRequestClose/
20
- // onOrientationChange) are real ViewConfig DirectEvents, so they too ride passthrough raw.
21
- // className is pulled out here, like style, and applied to the CONTAINER element below — left in
22
- // ...passthrough it would land on the outer modal host instead (renderModal composes
23
- // `style` into the container's style, not the host's).
24
- const { visible, transparent, backdropColor, animationType, presentationStyle, supportedOrientations, hardwareAccelerated, statusBarTranslucent, navigationBarTranslucent, allowSwipeDismissal, style, className, children, onDismiss, ...passthrough } = resolveAccessibilityProps(rawProps);
25
- // The iOS keep-alive (state/modal.ts): armed on show, dropped only by the native dismiss below.
15
+ import { dlog, Platform, } from '@symbiote-native/engine';
16
+ import { createInitialModalState, isModalVisible, modalReducer, modalVisibilityAction, renderModal, resolveAccessibilityProps, shouldRenderModal, warnAboutModalProps, } from '@symbiote-native/components';
17
+ import { VirtualizedListScopeResetter } from '../virtualized-list/nested-scope.js';
18
+ // The user children nest UNDER the container View, never beside the host, as RN lays a modal out
19
+ // They sit outside the list above, so a list in them is not nested in it
20
+ function modalTreeOf(input) {
21
+ const { className, children, ...renderInput } = input;
22
+ const root = renderModal(renderInput);
23
+ const [container] = root.children;
24
+ if (typeof container === 'string')
25
+ return null;
26
+ return createElement(root.type, { key: root.key, ...root.props }, createElement(container.type, { key: container.key, ...container.props, className }, createElement(VirtualizedListScopeResetter, null, children)));
27
+ }
28
+ // The iOS keep-alive: armed on show, dropped only by the native dismiss
29
+ function useKeepAlive(visible) {
26
30
  const isVisible = isModalVisible(visible);
27
31
  const [state, dispatch] = useReducer(modalReducer, isVisible, createInitialModalState);
28
32
  useEffect(() => {
@@ -30,6 +34,35 @@ export const Modal = rawProps => {
30
34
  if (action !== undefined)
31
35
  dispatch(action);
32
36
  }, [isVisible]);
37
+ return { isVisible, state, dispatch };
38
+ }
39
+ // RN checks on mount and on every update, a dev build only
40
+ function useModalWarnings(props) {
41
+ const { presentationStyle, transparent, navigationBarTranslucent, statusBarTranslucent, allowSwipeDismissal, onRequestClose, } = props;
42
+ useEffect(() => {
43
+ warnAboutModalProps({
44
+ presentationStyle,
45
+ transparent,
46
+ navigationBarTranslucent,
47
+ statusBarTranslucent,
48
+ allowSwipeDismissal,
49
+ onRequestClose,
50
+ });
51
+ }, [
52
+ presentationStyle,
53
+ transparent,
54
+ navigationBarTranslucent,
55
+ statusBarTranslucent,
56
+ allowSwipeDismissal,
57
+ onRequestClose,
58
+ ]);
59
+ }
60
+ export const Modal = rawProps => {
61
+ useModalWarnings(rawProps);
62
+ // Aria and role fold here, the events are real DirectEvents and ride `passthrough` raw
63
+ // `className` is pulled out to land on the container, in `passthrough` it would reach the host
64
+ const { visible, transparent, backdropColor, animationType, presentationStyle, supportedOrientations, hardwareAccelerated, statusBarTranslucent, navigationBarTranslucent, allowSwipeDismissal, style, className, children, onDismiss, ...passthrough } = resolveAccessibilityProps(rawProps);
65
+ const { isVisible, state, dispatch } = useKeepAlive(visible);
33
66
  if (!shouldRenderModal(isVisible, state)) {
34
67
  dlog('Modal hidden -> no node committed');
35
68
  return null;
@@ -41,7 +74,7 @@ export const Modal = rawProps => {
41
74
  dispatch({ type: 'hide' });
42
75
  onDismiss?.();
43
76
  };
44
- const root = renderModal({
77
+ return modalTreeOf({
45
78
  visible,
46
79
  transparent,
47
80
  backdropColor,
@@ -54,11 +87,7 @@ export const Modal = rawProps => {
54
87
  allowSwipeDismissal,
55
88
  style,
56
89
  passthrough: { ...passthrough, onDismiss: handleDismiss },
90
+ className,
91
+ children,
57
92
  });
58
- // root = modal > [container]; the user children nest UNDER the container View, never as
59
- // a direct sibling of the host (RN's modal content layout).
60
- const [container] = root.children;
61
- if (typeof container === 'string')
62
- return null;
63
- return createElement(root.type, { key: root.key, ...root.props }, createElement(container.type, { key: container.key, ...container.props, className }, children));
64
93
  };
@@ -1,17 +1,18 @@
1
1
  import type { ReactNode } from 'react';
2
2
  import type { IAccessibilityProps, IAriaProps } from '@symbiote-native/components';
3
- export interface IRefreshControlProps extends IAccessibilityProps, IAriaProps {
3
+ import type { IColorValue } from '@symbiote-native/engine';
4
+ export type IRefreshControlProps = IAccessibilityProps & IAriaProps & {
4
5
  refreshing: boolean;
5
6
  id?: string;
6
7
  onRefresh?: () => void | Promise<void>;
7
- tintColor?: string;
8
+ tintColor?: IColorValue;
8
9
  title?: string;
9
- titleColor?: string;
10
+ titleColor?: IColorValue;
10
11
  progressViewOffset?: number;
11
- colors?: readonly string[];
12
- progressBackgroundColor?: string;
12
+ colors?: readonly IColorValue[];
13
+ progressBackgroundColor?: IColorValue;
13
14
  size?: 'default' | 'large';
14
15
  enabled?: boolean;
15
16
  children?: ReactNode;
16
17
  className?: string;
17
- }
18
+ };