@symbiote-native/react 3.3.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 (76) hide show
  1. package/build/batched-updates.d.ts +2 -0
  2. package/build/batched-updates.js +5 -0
  3. package/build/components/flat-list/index.d.ts +16 -13
  4. package/build/components/flat-list/index.js +86 -72
  5. package/build/components/input-accessory-view-props.d.ts +4 -3
  6. package/build/components/keyboard-avoiding-view/index.d.ts +2 -2
  7. package/build/components/keyboard-avoiding-view/index.js +45 -84
  8. package/build/components/layout-conformance-props.d.ts +6 -0
  9. package/build/components/layout-conformance-props.js +1 -0
  10. package/build/components/modal/index.d.ts +4 -4
  11. package/build/components/modal/index.js +47 -18
  12. package/build/components/refresh-control-props.d.ts +7 -6
  13. package/build/components/scroll-view/scroll-view-props.d.ts +8 -5
  14. package/build/components/text-input/text-input-props.d.ts +4 -0
  15. package/build/components/touchable/touchable-props.d.ts +6 -5
  16. package/build/components/virtualized-list/index.d.ts +3 -70
  17. package/build/components/virtualized-list/index.js +50 -548
  18. package/build/components/virtualized-list/list-children.d.ts +13 -0
  19. package/build/components/virtualized-list/list-children.js +79 -0
  20. package/build/components/virtualized-list/list-config.d.ts +11 -0
  21. package/build/components/virtualized-list/list-config.js +14 -0
  22. package/build/components/virtualized-list/list-elements.d.ts +11 -0
  23. package/build/components/virtualized-list/list-elements.js +26 -0
  24. package/build/components/virtualized-list/list-host.d.ts +14 -0
  25. package/build/components/virtualized-list/list-host.js +24 -0
  26. package/build/components/virtualized-list/list-scroll-props.d.ts +32 -0
  27. package/build/components/virtualized-list/list-scroll-props.js +30 -0
  28. package/build/components/virtualized-list/nested-scope.d.ts +10 -0
  29. package/build/components/virtualized-list/nested-scope.js +16 -0
  30. package/build/components/virtualized-list/use-list-driver.d.ts +22 -0
  31. package/build/components/virtualized-list/use-list-driver.js +104 -0
  32. package/build/components/virtualized-list/use-list-nesting.d.ts +12 -0
  33. package/build/components/virtualized-list/use-list-nesting.js +37 -0
  34. package/build/components/virtualized-list/use-list-parts.d.ts +12 -0
  35. package/build/components/virtualized-list/use-list-parts.js +20 -0
  36. package/build/components/virtualized-list/use-scroll-handlers.d.ts +17 -0
  37. package/build/components/virtualized-list/use-scroll-handlers.js +106 -0
  38. package/build/components/virtualized-list/use-separators.d.ts +8 -0
  39. package/build/components/virtualized-list/use-separators.js +17 -0
  40. package/build/components/virtualized-list/virtualized-list-props.d.ts +85 -0
  41. package/build/components/virtualized-list/virtualized-list-props.js +2 -0
  42. package/build/components/virtualized-section-list/index.d.ts +24 -8
  43. package/build/components/virtualized-section-list/index.js +89 -93
  44. package/build/components/virtualized-section-list/section-item-cell.d.ts +23 -0
  45. package/build/components/virtualized-section-list/section-item-cell.js +48 -0
  46. package/build/components.d.ts +5 -5
  47. package/build/hooks/use-animated-value.d.ts +7 -0
  48. package/build/hooks/use-animated-value.js +19 -0
  49. package/build/hooks/use-color-scheme.d.ts +2 -2
  50. package/build/hooks/use-color-scheme.js +4 -13
  51. package/build/hooks/use-pressability.d.ts +34 -0
  52. package/build/hooks/use-pressability.js +39 -0
  53. package/build/hooks/use-window-dimensions.js +6 -10
  54. package/build/host-config.js +6 -5
  55. package/build/index.d.ts +19 -5
  56. package/build/index.js +20 -14
  57. package/build/jsx-runtime.d.ts +15 -16
  58. package/build/modules/animated/create-animated-component.d.ts +2 -2
  59. package/build/modules/animated/create-animated-component.js +10 -22
  60. package/build/modules/animated/index.d.ts +54 -41
  61. package/build/modules/animated/index.js +2 -27
  62. package/build/modules/app-registry/index.d.ts +3 -3
  63. package/build/modules/app-registry/index.js +2 -2
  64. package/build/modules/status-bar/index.android.d.ts +1 -1
  65. package/build/modules/status-bar/index.android.js +7 -27
  66. package/build/modules/status-bar/index.ios.d.ts +1 -1
  67. package/build/modules/status-bar/index.ios.js +4 -28
  68. package/build/modules/status-bar/shared.d.ts +5 -4
  69. package/build/modules/status-bar/shared.js +33 -6
  70. package/build/native-host-tags.d.ts +2 -0
  71. package/build/native-host-tags.js +4 -0
  72. package/build/render.d.ts +1 -1
  73. package/build/render.js +3 -1
  74. package/build/root-tag-context.d.ts +1 -0
  75. package/build/root-tag-context.js +4 -0
  76. package/package.json +6 -6
@@ -0,0 +1,85 @@
1
+ import type { ComponentType, ReactElement, ReactNode } from 'react';
2
+ import type { ISymbioteEvent } from '@symbiote-native/engine';
3
+ import type { IAccessibilityProps, IAriaProps, ICellRendererBaseProps, IInnerViewRef, ISeparatorProps, ISeparators, IViewabilityConfig, IViewabilityConfigCallbackPair, IViewableItemsChangedInfo } from '@symbiote-native/components';
4
+ import type { IStyleProp, IViewStyle } from '../../utils/styles';
5
+ export type IListItemInfo<ItemT> = {
6
+ item: ItemT;
7
+ index: number;
8
+ separators: ISeparators;
9
+ };
10
+ export type IRenderItem<ItemT> = (info: IListItemInfo<ItemT>) => ReactNode;
11
+ export type ICellRendererProps<ItemT> = ICellRendererBaseProps<ItemT> & {
12
+ children?: ReactNode;
13
+ };
14
+ export type ICellRendererComponent<ItemT> = (props: ICellRendererProps<ItemT>) => ReactNode;
15
+ export type IListSlot = ComponentType<Record<string, never>> | ReactElement | undefined;
16
+ export type ISeparatorComponent<ItemT> = ComponentType<ISeparatorProps<ItemT>> | ReactElement;
17
+ export type IVirtualizedListProps<ItemT> = IAccessibilityProps & IAriaProps & {
18
+ data: unknown;
19
+ getItem: (data: unknown, index: number) => ItemT;
20
+ getItemCount: (data: unknown) => number;
21
+ renderItem?: IRenderItem<ItemT>;
22
+ ListItemComponent?: ComponentType<IListItemInfo<ItemT>>;
23
+ keyExtractor?: (item: ItemT, index: number) => string;
24
+ getItemLayout?: (data: unknown, index: number) => {
25
+ length: number;
26
+ offset: number;
27
+ index: number;
28
+ };
29
+ ItemSeparatorComponent?: ISeparatorComponent<ItemT>;
30
+ CellRendererComponent?: ICellRendererComponent<ItemT>;
31
+ ListHeaderComponent?: IListSlot;
32
+ ListFooterComponent?: IListSlot;
33
+ ListEmptyComponent?: IListSlot;
34
+ ListHeaderComponentStyle?: IStyleProp<IViewStyle>;
35
+ ListFooterComponentStyle?: IStyleProp<IViewStyle>;
36
+ horizontal?: boolean;
37
+ inverted?: boolean;
38
+ extraData?: unknown;
39
+ onEndReached?: (info: {
40
+ distanceFromEnd: number;
41
+ }) => void;
42
+ onEndReachedThreshold?: number;
43
+ onStartReached?: (info: {
44
+ distanceFromStart: number;
45
+ }) => void;
46
+ onStartReachedThreshold?: number;
47
+ onRefresh?: () => void;
48
+ refreshing?: boolean | null;
49
+ progressViewOffset?: number;
50
+ onViewableItemsChanged?: (info: IViewableItemsChangedInfo<ItemT>) => void;
51
+ viewabilityConfig?: IViewabilityConfig;
52
+ viewabilityConfigCallbackPairs?: IViewabilityConfigCallbackPair<ItemT>[];
53
+ onScrollToIndexFailed?: (info: {
54
+ index: number;
55
+ highestMeasuredFrameIndex: number;
56
+ averageItemLength: number;
57
+ }) => void;
58
+ initialNumToRender?: number;
59
+ initialScrollIndex?: number;
60
+ maxToRenderPerBatch?: number;
61
+ updateCellsBatchingPeriod?: number;
62
+ windowSize?: number;
63
+ disableVirtualization?: boolean;
64
+ stickyHeaderIndices?: number[];
65
+ maintainVisibleContentPosition?: {
66
+ minIndexForVisible: number;
67
+ autoscrollToTopThreshold?: number;
68
+ };
69
+ onScroll?: (event: ISymbioteEvent) => void;
70
+ onScrollBeginDrag?: (event: ISymbioteEvent) => void;
71
+ onScrollEndDrag?: (event: ISymbioteEvent) => void;
72
+ onMomentumScrollBegin?: (event: ISymbioteEvent) => void;
73
+ onMomentumScrollEnd?: (event: ISymbioteEvent) => void;
74
+ onContentSizeChange?: (width: number, height: number) => void;
75
+ scrollEventThrottle?: number;
76
+ keyboardShouldPersistTaps?: boolean | 'always' | 'never' | 'handled';
77
+ keyboardDismissMode?: 'none' | 'on-drag' | 'interactive';
78
+ removeClippedSubviews?: boolean;
79
+ nestedScrollEnabled?: boolean;
80
+ stickyHeaderHiddenOnScroll?: boolean;
81
+ innerViewRef?: IInnerViewRef;
82
+ style?: IStyleProp<IViewStyle>;
83
+ contentContainerStyle?: IStyleProp<IViewStyle>;
84
+ className?: string;
85
+ };
@@ -0,0 +1,2 @@
1
+ // Public prop types of the React VirtualizedList
2
+ export {};
@@ -1,12 +1,20 @@
1
1
  import { type ComponentType, type ReactElement, type ReactNode, type Ref } from 'react';
2
2
  import { type ISymbioteEvent } from '@symbiote-native/engine';
3
- import { type ISection, type IVirtualizedSectionListHandle } from '@symbiote-native/components';
4
- import { type ISeparators, type ISeparatorProps } from '../virtualized-list';
5
- import type { IAccessibilityProps, IAriaProps } from '@symbiote-native/components';
3
+ import { type ISection as ICoreSection, type IVirtualizedSectionListHandle } from '@symbiote-native/components';
4
+ import { type ICellRendererComponent, type ISeparatorComponent, type ISeparators } from '../virtualized-list';
5
+ import type { IAccessibilityProps, IAriaProps, IInnerViewRef } from '@symbiote-native/components';
6
6
  import type { IStyleProp, IViewStyle } from '../../utils/styles';
7
- export type { ISection } from '@symbiote-native/components';
8
7
  export type { IVirtualizedSectionListHandle };
9
- export interface IVirtualizedSectionListProps<ItemT> extends IAccessibilityProps, IAriaProps {
8
+ export type ISection<ItemT> = ICoreSection<ItemT> & {
9
+ renderItem?: (info: {
10
+ item: ItemT;
11
+ index: number;
12
+ section: ISection<ItemT>;
13
+ separators: ISeparators;
14
+ }) => ReactNode;
15
+ ItemSeparatorComponent?: ISeparatorComponent<ItemT>;
16
+ };
17
+ export type IVirtualizedSectionListProps<ItemT> = IAccessibilityProps & IAriaProps & {
10
18
  sections: ReadonlyArray<ISection<ItemT>>;
11
19
  renderItem: (info: {
12
20
  item: ItemT;
@@ -20,7 +28,7 @@ export interface IVirtualizedSectionListProps<ItemT> extends IAccessibilityProps
20
28
  renderSectionFooter?: (info: {
21
29
  section: ISection<ItemT>;
22
30
  }) => ReactNode;
23
- SectionSeparatorComponent?: ComponentType<Record<string, never>> | ReactElement;
31
+ SectionSeparatorComponent?: ISeparatorComponent<ItemT>;
24
32
  keyExtractor?: (item: ItemT, index: number) => string;
25
33
  getItemLayout?: (data: ReadonlyArray<ISection<ItemT>> | null, index: number) => {
26
34
  length: number;
@@ -29,10 +37,13 @@ export interface IVirtualizedSectionListProps<ItemT> extends IAccessibilityProps
29
37
  };
30
38
  stickySectionHeadersEnabled?: boolean;
31
39
  extraData?: unknown;
32
- ItemSeparatorComponent?: ComponentType<ISeparatorProps<ItemT>>;
40
+ ItemSeparatorComponent?: ISeparatorComponent<ItemT>;
41
+ CellRendererComponent?: ICellRendererComponent<unknown>;
33
42
  ListHeaderComponent?: ComponentType<Record<string, never>> | ReactElement;
34
43
  ListFooterComponent?: ComponentType<Record<string, never>> | ReactElement;
35
44
  ListEmptyComponent?: ComponentType<Record<string, never>> | ReactElement;
45
+ ListHeaderComponentStyle?: IStyleProp<IViewStyle>;
46
+ ListFooterComponentStyle?: IStyleProp<IViewStyle>;
36
47
  onEndReached?: (info: {
37
48
  distanceFromEnd: number;
38
49
  }) => void;
@@ -49,7 +60,9 @@ export interface IVirtualizedSectionListProps<ItemT> extends IAccessibilityProps
49
60
  maxToRenderPerBatch?: number;
50
61
  updateCellsBatchingPeriod?: number;
51
62
  windowSize?: number;
63
+ disableVirtualization?: boolean;
52
64
  inverted?: boolean;
65
+ horizontal?: boolean;
53
66
  maintainVisibleContentPosition?: {
54
67
  minIndexForVisible: number;
55
68
  autoscrollToTopThreshold?: number;
@@ -59,15 +72,18 @@ export interface IVirtualizedSectionListProps<ItemT> extends IAccessibilityProps
59
72
  onScrollEndDrag?: (event: ISymbioteEvent) => void;
60
73
  onMomentumScrollBegin?: (event: ISymbioteEvent) => void;
61
74
  onMomentumScrollEnd?: (event: ISymbioteEvent) => void;
75
+ onContentSizeChange?: (width: number, height: number) => void;
62
76
  scrollEventThrottle?: number;
63
77
  keyboardShouldPersistTaps?: boolean | 'always' | 'never' | 'handled';
64
78
  keyboardDismissMode?: 'none' | 'on-drag' | 'interactive';
65
79
  removeClippedSubviews?: boolean;
66
80
  nestedScrollEnabled?: boolean;
81
+ stickyHeaderHiddenOnScroll?: boolean;
82
+ innerViewRef?: IInnerViewRef;
67
83
  style?: IStyleProp<IViewStyle>;
68
84
  contentContainerStyle?: IStyleProp<IViewStyle>;
69
85
  className?: string;
70
- }
86
+ };
71
87
  export declare function VirtualizedSectionList<ItemT>(props: IVirtualizedSectionListProps<ItemT> & {
72
88
  ref?: Ref<IVirtualizedSectionListHandle>;
73
89
  }): ReactElement;
@@ -5,113 +5,109 @@
5
5
  // mapping are shared from @symbiote-native/components; this file wires React's lifecycle (refs +
6
6
  // useImperativeHandle + the per-entry render dispatch). Lower layer in RN's
7
7
  // SectionList -> VirtualizedSectionList -> VirtualizedList stack.
8
- import { createElement, useImperativeHandle, useRef, } from 'react';
9
- import { dlog, Platform, } from '@symbiote-native/engine';
10
- import { flattenSections, resolveStickySectionHeaders, scrollLocationToFlatIndex, sectionEntryKey, unwrapEntryItem, } from '@symbiote-native/components';
8
+ import { createElement, useImperativeHandle, useRef, useState, } from 'react';
9
+ import { dlog, Platform } from '@symbiote-native/engine';
10
+ import { cellGapsFor, createSeparatorBoard, flattenSections, resolveStickySectionHeaders, layoutOverSections, renderSectionEntry, resolveScrollLocation, routeScrollHandle, sectionEntryKey, } from '@symbiote-native/components';
11
11
  import { VirtualizedList, } from '../virtualized-list/index.js';
12
- function resolveSeparator(component) {
13
- if (component === undefined)
14
- return undefined;
15
- if (typeof component === 'function')
16
- return createElement(component, {});
17
- return component;
18
- }
19
- export function VirtualizedSectionList(props) {
20
- const { ref, sections, renderItem, renderSectionHeader, renderSectionFooter, SectionSeparatorComponent,
21
- // Pulled out of `rest`: the user's separator is typed on ItemT, but the inner VirtualizedList
22
- // streams ISectionEntry<ItemT>, so we wrap it to unwrap each entry back to its ItemT.
23
- ItemSeparatorComponent, keyExtractor,
24
- // Also pulled out of `rest`: the inner list streams entries, the user's callback expects
25
- // sections. See entryItemLayout below.
26
- getItemLayout, stickySectionHeadersEnabled, ...rest } = props;
27
- const { entries, headerIndices } = flattenSections(sections, SectionSeparatorComponent !== undefined);
28
- // RN sticks section headers by default only on iOS; Android does not unless asked. headerIndices
29
- // are the flat positions of every section header; VirtualizedList forwards the in-window ones.
30
- const stickyHeaderIndices = resolveStickySectionHeaders(stickySectionHeadersEnabled, headerIndices, Platform.OS);
31
- // The handle reaches into the inner VirtualizedList to drive scrollToIndex.
32
- const listRef = useRef(null);
33
- dlog(`VirtualizedSectionList: ${sections.length} sections flattened to ${entries.length} entries`);
34
- useImperativeHandle(ref ?? null, () => ({
12
+ import { SectionItemCell } from './section-item-cell.js';
13
+ // Kept apart so the component body stays readable: the handle only forwards to the inner list
14
+ function buildSectionHandle(listRef, headerIndices, stickyHeaderIndices) {
15
+ return {
35
16
  scrollToLocation: (params) => {
36
- const flatIndex = scrollLocationToFlatIndex(headerIndices, params.sectionIndex, params.itemIndex);
37
- if (flatIndex === undefined) {
17
+ const target = resolveScrollLocation(headerIndices, stickyHeaderIndices, params);
18
+ if (target === undefined) {
38
19
  dlog(`VirtualizedSectionList scrollToLocation: section ${params.sectionIndex} out of range`);
39
20
  return;
40
21
  }
41
22
  dlog(`VirtualizedSectionList scrollToLocation section=${params.sectionIndex} ` +
42
- `item=${params.itemIndex} -> flat ${flatIndex}`);
43
- listRef.current?.scrollToIndex({
44
- index: flatIndex,
45
- viewOffset: params.viewOffset,
46
- viewPosition: params.viewPosition,
47
- animated: params.animated,
48
- });
49
- },
50
- flashScrollIndicators: () => {
51
- listRef.current?.flashScrollIndicators();
52
- },
53
- getNativeScrollRef: () => listRef.current?.getNativeScrollRef() ?? null,
54
- getScrollableNode: () => listRef.current?.getScrollableNode() ?? null,
55
- getScrollResponder: () => listRef.current?.getScrollResponder() ?? null,
56
- getScrollNode: () => listRef.current?.getScrollNode() ?? null,
57
- recordInteraction: () => {
58
- listRef.current?.recordInteraction();
23
+ `item=${params.itemIndex} -> flat ${target.index}`);
24
+ listRef.current?.scrollToIndex(target);
59
25
  },
60
- }), [headerIndices]);
61
- const renderEntry = (info) => {
62
- const entry = info.item;
63
- if (entry.kind === 'header') {
64
- return renderSectionHeader
65
- ? renderSectionHeader({ section: entry.section })
66
- : undefined;
67
- }
68
- if (entry.kind === 'footer') {
69
- return renderSectionFooter
70
- ? renderSectionFooter({ section: entry.section })
71
- : undefined;
72
- }
73
- if (entry.kind === 'section-separator') {
74
- return resolveSeparator(SectionSeparatorComponent);
75
- }
76
- return renderItem({
77
- item: entry.item,
78
- index: entry.itemIndex,
79
- section: entry.section,
80
- separators: info.separators,
81
- });
26
+ ...routeScrollHandle(() => listRef.current),
82
27
  };
83
- // The user's ItemSeparatorComponent is typed on ItemT; the inner stream is the entry wrapper, so
84
- // unwrap each entry back to its ItemT (shared unwrapEntryItem) before handing it to the user.
85
- const entrySeparatorComponent = ItemSeparatorComponent === undefined
86
- ? undefined
87
- : (entryProps) => createElement(ItemSeparatorComponent, {
88
- ...entryProps,
89
- leadingItem: unwrapEntryItem(entryProps.leadingItem),
90
- trailingItem: unwrapEntryItem(entryProps.trailingItem),
91
- });
92
- const entryKeyExtractor = (entry, index) => sectionEntryKey(entry, index, keyExtractor);
93
- // Hand the callback `sections`, not the entries: RN's inner VirtualizedList gets
94
- // `data={this.props.sections}` (VirtualizedSectionList.js:216) while ours streams the FLATTENED
95
- // entries, so the same user code would otherwise see a different argument here than on RN.
96
- //
97
- // UPSTREAM-DIVERGENCE(react-native): the flat INDEX matches RN's (two rows per section, header
98
- // and footer) only while SectionSeparatorComponent is unset. With it, flattenSections emits an
99
- // extra 'section-separator' row per boundary that RN renders inside the neighbouring cell, so
100
- // indices shift by one per boundary from the second section on. Deliberate - that row is how this
101
- // adapter paints the separator; a caller combining the two must account for it.
102
- const entryItemLayout = getItemLayout === undefined
103
- ? undefined
104
- : (_entries, index) => getItemLayout(sections, index);
28
+ }
29
+ // The handle reaches into the inner list to drive `scrollToIndex`, so it owns the list ref
30
+ function useSectionHandle(ref, headerIndices, stickyHeaderIndices) {
31
+ const listRef = useRef(null);
32
+ useImperativeHandle(ref ?? null, () => buildSectionHandle(listRef, headerIndices, stickyHeaderIndices), [headerIndices, stickyHeaderIndices]);
33
+ return listRef;
34
+ }
35
+ function flattenLogged(sections) {
36
+ const flat = flattenSections(sections);
37
+ dlog(`VirtualizedSectionList: ${sections.length} sections flattened to ${flat.entries.length} entries`);
38
+ return flat;
39
+ }
40
+ // The flat stream plus the board its item cells share their separator state through
41
+ function useSectionEntries(sections, keyExtractor, stickySectionHeadersEnabled) {
42
+ const [board] = useState(() => createSeparatorBoard());
43
+ const keyOf = (entry) => sectionEntryKey(entry, keyExtractor);
44
+ const { entries, headerIndices } = flattenLogged(sections);
45
+ // RN sticks section headers by default only on iOS; Android does not unless asked
46
+ const stickyHeaderIndices = resolveStickySectionHeaders(stickySectionHeadersEnabled, headerIndices, Platform.OS);
47
+ return { entries, headerIndices, stickyHeaderIndices, board, keyOf };
48
+ }
49
+ // A separator component by the kind of gap it sits in: the section one, or the item one where a
50
+ // section's own `ItemSeparatorComponent` beats the list's
51
+ function separatorFor(gap, renderers) {
52
+ if (gap.kind === 'section')
53
+ return renderers.SectionSeparatorComponent;
54
+ if (gap.kind === 'item')
55
+ return (gap.props.section.ItemSeparatorComponent ??
56
+ renderers.ItemSeparatorComponent);
57
+ return undefined;
58
+ }
59
+ // The inner list's single `renderItem`, one renderer per entry kind. An item entry becomes a
60
+ // cell that paints its own separators, so the inner list gets no `ItemSeparatorComponent`
61
+ function entryRendererFor(renderers) {
62
+ return info => renderSectionEntry({
63
+ header: ({ section }) => renderers.renderSectionHeader?.({ section }),
64
+ footer: ({ section }) => renderers.renderSectionFooter?.({ section }),
65
+ item: entry => {
66
+ const gaps = cellGapsFor(renderers.entries, info.index);
67
+ const previous = renderers.entries[info.index - 1];
68
+ return createElement((SectionItemCell), {
69
+ board: renderers.board,
70
+ cellKey: renderers.keyOf(entry),
71
+ prevCellKey: previous === undefined ? undefined : renderers.keyOf(previous),
72
+ item: entry.item,
73
+ index: entry.itemIndex,
74
+ section: entry.section,
75
+ renderItem: entry.section.renderItem ?? renderers.renderItem,
76
+ leadingGap: gaps.leading,
77
+ trailingGap: gaps.trailing,
78
+ LeadingSeparatorComponent: separatorFor(gaps.leading, renderers),
79
+ TrailingSeparatorComponent: separatorFor(gaps.trailing, renderers),
80
+ inverted: renderers.inverted === true,
81
+ });
82
+ },
83
+ }, info.item);
84
+ }
85
+ export function VirtualizedSectionList(props) {
86
+ const { ref, sections, renderItem, renderSectionHeader, renderSectionFooter, SectionSeparatorComponent,
87
+ // Pulled out of `rest`, these are typed on ItemT while the inner list streams entries
88
+ ItemSeparatorComponent, keyExtractor, getItemLayout, stickySectionHeadersEnabled, inverted, ...rest } = props;
89
+ const { entries, headerIndices, stickyHeaderIndices, board, keyOf } = useSectionEntries(sections, keyExtractor, stickySectionHeadersEnabled);
90
+ const listRef = useSectionHandle(ref, headerIndices, stickyHeaderIndices);
105
91
  return createElement((VirtualizedList), {
106
92
  ref: listRef,
107
93
  data: entries,
108
94
  getItem: (_source, index) => entries[index],
109
95
  getItemCount: () => entries.length,
110
- renderItem: renderEntry,
111
- keyExtractor: entryKeyExtractor,
112
- getItemLayout: entryItemLayout,
96
+ renderItem: entryRendererFor({
97
+ renderItem,
98
+ renderSectionHeader,
99
+ renderSectionFooter,
100
+ SectionSeparatorComponent,
101
+ ItemSeparatorComponent,
102
+ inverted,
103
+ entries,
104
+ board,
105
+ keyOf,
106
+ }),
107
+ keyExtractor: keyOf,
108
+ getItemLayout: layoutOverSections(getItemLayout, sections),
113
109
  stickyHeaderIndices,
114
- ItemSeparatorComponent: entrySeparatorComponent,
110
+ inverted,
115
111
  ...rest,
116
112
  });
117
113
  }
@@ -0,0 +1,23 @@
1
+ import { type ReactElement, type ReactNode } from 'react';
2
+ import { type ISeparatorBoard, type ISeparatorGap } from '@symbiote-native/components';
3
+ import type { ISeparatorComponent, ISeparators } from '../virtualized-list';
4
+ export type ISectionItemCellProps<ItemT, SectionT> = {
5
+ board: ISeparatorBoard<Record<string, unknown>>;
6
+ cellKey: string;
7
+ prevCellKey: string | undefined;
8
+ item: ItemT;
9
+ index: number;
10
+ section: SectionT;
11
+ renderItem: (info: {
12
+ item: ItemT;
13
+ index: number;
14
+ section: SectionT;
15
+ separators: ISeparators;
16
+ }) => ReactNode;
17
+ leadingGap: ISeparatorGap<ItemT, SectionT>;
18
+ trailingGap: ISeparatorGap<ItemT, SectionT>;
19
+ LeadingSeparatorComponent: ISeparatorComponent<ItemT> | undefined;
20
+ TrailingSeparatorComponent: ISeparatorComponent<ItemT> | undefined;
21
+ inverted: boolean;
22
+ };
23
+ export declare function SectionItemCell<ItemT, SectionT>(props: ISectionItemCellProps<ItemT, SectionT>): ReactElement;
@@ -0,0 +1,48 @@
1
+ // Item cell with its separators around it, as in RN's `ItemWithSeparator`
2
+ // Their state sits in a shared board, т.к. `highlight()` also lights the previous cell's one
3
+ import { createElement, Fragment, isValidElement, useEffect, useSyncExternalStore, } from 'react';
4
+ import { createCellSeparators, } from '@symbiote-native/components';
5
+ function paint(separator) {
6
+ const { Component, gap, isHighlighted, override } = separator;
7
+ if (Component === undefined)
8
+ return null;
9
+ if (isValidElement(Component))
10
+ return Component;
11
+ return createElement(Component, {
12
+ highlighted: isHighlighted,
13
+ ...gap.props,
14
+ ...override,
15
+ });
16
+ }
17
+ export function SectionItemCell(props) {
18
+ const { board, cellKey, prevCellKey } = props;
19
+ const state = useSyncExternalStore(listener => board.subscribe(cellKey, listener), () => board.read(cellKey));
20
+ useEffect(() => () => board.release(cellKey), [board, cellKey]);
21
+ const separators = createCellSeparators(board, () => ({
22
+ cellKey,
23
+ prevCellKey,
24
+ has: {
25
+ leading: props.LeadingSeparatorComponent !== undefined,
26
+ trailing: props.TrailingSeparatorComponent !== undefined,
27
+ },
28
+ }));
29
+ const leading = paint({
30
+ Component: props.LeadingSeparatorComponent,
31
+ gap: props.leadingGap,
32
+ isHighlighted: state.leadingHighlighted,
33
+ override: state.leadingOverride,
34
+ });
35
+ const trailing = paint({
36
+ Component: props.TrailingSeparatorComponent,
37
+ gap: props.trailingGap,
38
+ isHighlighted: state.trailingHighlighted,
39
+ override: state.trailingOverride,
40
+ });
41
+ const element = props.renderItem({
42
+ item: props.item,
43
+ index: props.index,
44
+ section: props.section,
45
+ separators,
46
+ });
47
+ return createElement(Fragment, null, props.inverted ? trailing : leading, element, props.inverted ? leading : trailing);
48
+ }
@@ -1,10 +1,10 @@
1
1
  import type { Ref, ReactNode } from 'react';
2
2
  import type { ISymbioteEvent } from '@symbiote-native/engine';
3
3
  import type { IHostInstance } from './host-instance';
4
- import type { IAccessibilityProps, IAriaProps } from '@symbiote-native/components';
4
+ import type { IAccessibilityProps, IAriaProps, ITextNativeOnlyProps } from '@symbiote-native/components';
5
5
  import type { IResponderProps } from './utils/responder-props';
6
6
  import type { IStyleProp, ITextStyle, IViewStyle } from './utils/styles';
7
- export interface IViewProps extends IAccessibilityProps, IAriaProps, IResponderProps {
7
+ export type IViewProps = IAccessibilityProps & IAriaProps & IResponderProps & {
8
8
  style?: IStyleProp<IViewStyle>;
9
9
  className?: string;
10
10
  onPress?: (event: ISymbioteEvent) => void;
@@ -29,8 +29,8 @@ export interface IViewProps extends IAccessibilityProps, IAriaProps, IResponderP
29
29
  needsOffscreenAlphaCompositing?: boolean;
30
30
  ref?: Ref<IHostInstance>;
31
31
  children?: ReactNode;
32
- }
33
- export interface ITextProps extends IAccessibilityProps, IAriaProps {
32
+ };
33
+ export type ITextProps = IAccessibilityProps & IAriaProps & ITextNativeOnlyProps & {
34
34
  style?: IStyleProp<ITextStyle>;
35
35
  className?: string;
36
36
  onPress?: (event: ISymbioteEvent) => void;
@@ -50,4 +50,4 @@ export interface ITextProps extends IAccessibilityProps, IAriaProps {
50
50
  selectionColor?: string;
51
51
  ref?: Ref<IHostInstance>;
52
52
  children?: ReactNode;
53
- }
53
+ };
@@ -0,0 +1,7 @@
1
+ import { type AnimatedColor, type AnimatedValue, type AnimatedValueXY, type IAnimatedValueConfig, type IColorInput } from '@symbiote-native/engine';
2
+ export declare function useAnimatedValue(initialValue: number, config?: IAnimatedValueConfig): AnimatedValue;
3
+ export declare function useAnimatedValueXY(initialValue: {
4
+ x: number;
5
+ y: number;
6
+ }, config?: IAnimatedValueConfig): AnimatedValueXY;
7
+ export declare function useAnimatedColor(inputValue?: IColorInput, config?: IAnimatedValueConfig): AnimatedColor;
@@ -0,0 +1,19 @@
1
+ // RN's `useAnimatedValue`, `useAnimatedValueXY` and `useAnimatedColor`: the instance is built on
2
+ // the first render and kept, so later renders ignore their arguments
3
+ import { useRef } from 'react';
4
+ import { createAnimatedColor, createAnimatedValue, createAnimatedValueXY, } from '@symbiote-native/engine';
5
+ // `useRef(factory())` would build a throwaway instance on every render
6
+ function useOnce(create) {
7
+ const ref = useRef(null);
8
+ ref.current ??= create();
9
+ return ref.current;
10
+ }
11
+ export function useAnimatedValue(initialValue, config) {
12
+ return useOnce(() => createAnimatedValue(initialValue, config));
13
+ }
14
+ export function useAnimatedValueXY(initialValue, config) {
15
+ return useOnce(() => createAnimatedValueXY(initialValue, config));
16
+ }
17
+ export function useAnimatedColor(inputValue, config) {
18
+ return useOnce(() => createAnimatedColor(inputValue, config));
19
+ }
@@ -1,2 +1,2 @@
1
- import { type IColorSchemeName } from '@symbiote-native/engine';
2
- export declare function useColorScheme(): IColorSchemeName | null;
1
+ import type { IColorSchemeName } from '@symbiote-native/engine';
2
+ export declare const useColorScheme: () => IColorSchemeName | null | undefined;
@@ -1,13 +1,4 @@
1
- // useColorScheme subscribes a component to the device color scheme and re-renders
2
- // on change. Mirrors RN's Libraries/Utilities/useColorScheme.js: a thin
3
- // useSyncExternalStore over Appearance's change event + current scheme read.
4
- import { useSyncExternalStore } from 'react';
5
- import { Appearance } from '@symbiote-native/engine';
6
- const subscribe = (onStoreChange) => {
7
- const subscription = Appearance.addChangeListener(onStoreChange);
8
- return () => subscription.remove();
9
- };
10
- const getSnapshot = () => Appearance.getColorScheme();
11
- export function useColorScheme() {
12
- return useSyncExternalStore(subscribe, getSnapshot);
13
- }
1
+ // RN's own `useColorScheme`, typed here because the source ships none
2
+ // @ts-expect-error - untyped Flow source
3
+ import useColorSchemeUpstream from 'react-native/Libraries/Utilities/useColorScheme';
4
+ export const useColorScheme = useColorSchemeUpstream;
@@ -0,0 +1,34 @@
1
+ import { type IPressabilityHandlers, type ISymbioteEvent } from '@symbiote-native/engine';
2
+ export type { IPressabilityHandlers } from '@symbiote-native/engine';
3
+ type IRectOrSize = number | {
4
+ top?: number;
5
+ left?: number;
6
+ bottom?: number;
7
+ right?: number;
8
+ };
9
+ type IPressHandler = (event: ISymbioteEvent) => unknown;
10
+ export type IPressabilityConfig = {
11
+ cancelable?: boolean | null;
12
+ disabled?: boolean | null;
13
+ hitSlop?: IRectOrSize | null;
14
+ pressRectOffset?: IRectOrSize | null;
15
+ android_disableSound?: boolean | null;
16
+ delayHoverIn?: number | null;
17
+ delayHoverOut?: number | null;
18
+ delayLongPress?: number | null;
19
+ delayPressIn?: number | null;
20
+ delayPressOut?: number | null;
21
+ minPressDuration?: number | null;
22
+ onBlur?: IPressHandler | null;
23
+ onFocus?: IPressHandler | null;
24
+ onHoverIn?: IPressHandler | null;
25
+ onHoverOut?: IPressHandler | null;
26
+ onLongPress?: IPressHandler | null;
27
+ onPress?: IPressHandler | null;
28
+ onPressIn?: IPressHandler | null;
29
+ onPressMove?: IPressHandler | null;
30
+ onPressOut?: IPressHandler | null;
31
+ blockNativeResponder?: boolean | null;
32
+ };
33
+ export declare function usePressability(config: IPressabilityConfig): IPressabilityHandlers;
34
+ export declare function usePressability(config: IPressabilityConfig | null | undefined): IPressabilityHandlers | null;
@@ -0,0 +1,39 @@
1
+ // RN's `usePressability`, over RN's own `Pressability` class, which `bootstrapHost` hands to the
2
+ // engine. RN's class expects its synthetic events, so each handler gets the event with `persist()`
3
+ import { useInsertionEffect, useRef } from 'react';
4
+ import { loadPressability, } from '@symbiote-native/engine';
5
+ // An event of ours answers everything RN's class reads, `persist` apart: nothing is pooled here
6
+ function withPersist(event) {
7
+ return Object.assign(Object.create(event), { persist: () => { } });
8
+ }
9
+ function persistingHandlers(handlers) {
10
+ return Object.fromEntries(Object.entries(handlers).map(([name, handler]) => [
11
+ name,
12
+ (event) => handler(withPersist(event)),
13
+ ]));
14
+ }
15
+ export function usePressability(config) {
16
+ const ref = useRef(null);
17
+ if (config != null && ref.current === null) {
18
+ const Pressability = loadPressability();
19
+ const pressability = new Pressability(config);
20
+ ref.current = {
21
+ pressability,
22
+ handlers: persistingHandlers(pressability.getEventHandlers()),
23
+ };
24
+ }
25
+ const instance = ref.current;
26
+ // A no-op on the first mount, on an update it points `Pressability` at the new config
27
+ useInsertionEffect(() => {
28
+ if (config != null && instance !== null) {
29
+ instance.pressability.configure(config);
30
+ }
31
+ }, [config, instance]);
32
+ // On unmount only, not when `config` changes: pending timers and state go
33
+ useInsertionEffect(() => {
34
+ if (instance === null)
35
+ return undefined;
36
+ return () => instance.pressability.reset();
37
+ }, [instance]);
38
+ return instance === null ? null : instance.handlers;
39
+ }
@@ -1,26 +1,22 @@
1
- // useWindowDimensions is the preferred API for components that need window metrics,
2
- // a port of RN's Libraries/Utilities/useWindowDimensions.js. It seeds from
3
- // Dimensions.get('window'), subscribes to 'change', and re-checks once after
4
- // subscribing to close the gap between the render-time get and the effect-time
5
- // listener. Only the window metrics changing triggers a re-render.
1
+ // Port of RN's `useWindowDimensions`: seeds from `Dimensions.get('window')`, subscribes to 'change'
2
+ // TODO(rn-port): RN's module loads `NativeDeviceInfo` eagerly, which needs a bridge itests lack
6
3
  import { useEffect, useState } from 'react';
7
4
  import { Dimensions, } from '@symbiote-native/engine';
8
5
  export function useWindowDimensions() {
9
6
  const [dimensions, setDimensions] = useState(() => Dimensions.get('window'));
10
7
  useEffect(() => {
11
8
  function handleChange(window) {
12
- if (dimensions.width !== window.width ||
9
+ const hasChanged = dimensions.width !== window.width ||
13
10
  dimensions.height !== window.height ||
14
11
  dimensions.scale !== window.scale ||
15
- dimensions.fontScale !== window.fontScale) {
12
+ dimensions.fontScale !== window.fontScale;
13
+ if (hasChanged)
16
14
  setDimensions(window);
17
- }
18
15
  }
19
16
  const subscription = Dimensions.addEventListener('change', (set) => {
20
17
  handleChange(set.window);
21
18
  });
22
- // We may have missed an update between calling `get` in render and subscribing
23
- // here; re-check now. If nothing changed, React filters the no-op set.
19
+ // An update may land between `get` in render and the subscription, so re-check now
24
20
  handleChange(Dimensions.get('window'));
25
21
  return () => {
26
22
  subscription.remove();