@ankhorage/surface 9.4.10 → 9.4.12

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/CHANGELOG.md CHANGED
@@ -1,5 +1,17 @@
1
1
  # @ankhorage/surface
2
2
 
3
+ ## 9.4.12
4
+
5
+ ### Patch Changes
6
+
7
+ - 5340e24: Support controlled pull-to-refresh state and callbacks through `ScrollView`.
8
+
9
+ ## 9.4.11
10
+
11
+ ### Patch Changes
12
+
13
+ - 45f3725: Keep responsive rendering stable through server hydration before applying browser viewport changes.
14
+
3
15
  ## 9.4.10
4
16
 
5
17
  ### Patch Changes
package/README.md CHANGED
@@ -3,7 +3,7 @@
3
3
 
4
4
  # @ankhorage/surface
5
5
 
6
- ![license: MIT](././paradox/badges/license.svg) ![npm: v9.4.10](././paradox/badges/npm.svg) ![runtime: bun](././paradox/badges/runtime.svg) ![typescript: strict](././paradox/badges/typescript.svg) ![eslint: checked](././paradox/badges/eslint.svg) ![prettier: checked](././paradox/badges/prettier.svg) ![build: checked](././paradox/badges/build.svg) ![tests: checked](././paradox/badges/tests.svg) ![paradox: warnings](././paradox/badges/docs.svg)
6
+ ![license: MIT](././paradox/badges/license.svg) ![npm: v9.4.12](././paradox/badges/npm.svg) ![runtime: bun](././paradox/badges/runtime.svg) ![typescript: strict](././paradox/badges/typescript.svg) ![eslint: checked](././paradox/badges/eslint.svg) ![prettier: checked](././paradox/badges/prettier.svg) ![build: checked](././paradox/badges/build.svg) ![tests: checked](././paradox/badges/tests.svg) ![paradox: warnings](././paradox/badges/docs.svg)
7
7
 
8
8
  Cross-platform UI foundation for React Native and React Native Web. Provides theming, layout primitives, and core building blocks for modern apps.
9
9
 
@@ -1 +1 @@
1
- {"version":3,"file":"ResponsiveProvider.d.ts","sourceRoot":"","sources":["../../../src/core/responsive/ResponsiveProvider.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAsC,MAAM,OAAO,CAAC;AAI3D,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,SAAS,CAAC;AAIjD,wBAAgB,kBAAkB,CAAC,EAAE,QAAQ,EAAE,EAAE;IAAE,QAAQ,EAAE,KAAK,CAAC,SAAS,CAAA;CAAE,qBAY7E;AAED,wBAAgB,oBAAoB,IAAI,iBAAiB,CAQxD"}
1
+ {"version":3,"file":"ResponsiveProvider.d.ts","sourceRoot":"","sources":["../../../src/core/responsive/ResponsiveProvider.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAsC,MAAM,OAAO,CAAC;AAG3D,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,SAAS,CAAC;AAKjD,wBAAgB,kBAAkB,CAAC,EAAE,QAAQ,EAAE,EAAE;IAAE,QAAQ,EAAE,KAAK,CAAC,SAAS,CAAA;CAAE,qBAY7E;AAED,wBAAgB,oBAAoB,IAAI,iBAAiB,CAQxD"}
@@ -1,9 +1,9 @@
1
1
  import React, { createContext, use, useMemo } from 'react';
2
- import { useWindowDimensions } from 'react-native';
3
2
  import { getBreakpointFromWidth } from './getBreakpointFromWidth';
3
+ import { useResponsiveWidth } from './useResponsiveWidth';
4
4
  const ResponsiveContext = createContext(null);
5
5
  export function ResponsiveProvider({ children }) {
6
- const { width } = useWindowDimensions();
6
+ const width = useResponsiveWidth();
7
7
  const value = useMemo(() => ({
8
8
  breakpoint: getBreakpointFromWidth(width),
9
9
  width,
@@ -1 +1 @@
1
- {"version":3,"file":"ResponsiveProvider.js","sourceRoot":"","sources":["../../../src/core/responsive/ResponsiveProvider.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,EAAE,aAAa,EAAE,GAAG,EAAE,OAAO,EAAE,MAAM,OAAO,CAAC;AAC3D,OAAO,EAAE,mBAAmB,EAAE,MAAM,cAAc,CAAC;AAEnD,OAAO,EAAE,sBAAsB,EAAE,MAAM,0BAA0B,CAAC;AAGlE,MAAM,iBAAiB,GAAG,aAAa,CAA2B,IAAI,CAAC,CAAC;AAExE,MAAM,UAAU,kBAAkB,CAAC,EAAE,QAAQ,EAAiC;IAC5E,MAAM,EAAE,KAAK,EAAE,GAAG,mBAAmB,EAAE,CAAC;IAExC,MAAM,KAAK,GAAG,OAAO,CACnB,GAAG,EAAE,CAAC,CAAC;QACL,UAAU,EAAE,sBAAsB,CAAC,KAAK,CAAC;QACzC,KAAK;KACN,CAAC,EACF,CAAC,KAAK,CAAC,CACR,CAAC;IAEF,OAAO,CAAC,iBAAiB,CAAC,KAAK,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,QAAQ,CAAC,EAAE,iBAAiB,CAAC,CAAC;AACzE,CAAC;AAED,MAAM,UAAU,oBAAoB;IAClC,MAAM,OAAO,GAAG,GAAG,CAAC,iBAAiB,CAAC,CAAC;IAEvC,IAAI,CAAC,OAAO,EAAE,CAAC;QACb,MAAM,IAAI,KAAK,CAAC,+DAA+D,CAAC,CAAC;IACnF,CAAC;IAED,OAAO,OAAO,CAAC;AACjB,CAAC","sourcesContent":["import React, { createContext, use, useMemo } from 'react';\nimport { useWindowDimensions } from 'react-native';\n\nimport { getBreakpointFromWidth } from './getBreakpointFromWidth';\nimport type { ResponsiveRuntime } from './types';\n\nconst ResponsiveContext = createContext<ResponsiveRuntime | null>(null);\n\nexport function ResponsiveProvider({ children }: { children: React.ReactNode }) {\n const { width } = useWindowDimensions();\n\n const value = useMemo<ResponsiveRuntime>(\n () => ({\n breakpoint: getBreakpointFromWidth(width),\n width,\n }),\n [width],\n );\n\n return <ResponsiveContext value={value}>{children}</ResponsiveContext>;\n}\n\nexport function useResponsiveRuntime(): ResponsiveRuntime {\n const runtime = use(ResponsiveContext);\n\n if (!runtime) {\n throw new Error('useResponsiveRuntime must be used within a ResponsiveProvider');\n }\n\n return runtime;\n}\n"]}
1
+ {"version":3,"file":"ResponsiveProvider.js","sourceRoot":"","sources":["../../../src/core/responsive/ResponsiveProvider.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,EAAE,aAAa,EAAE,GAAG,EAAE,OAAO,EAAE,MAAM,OAAO,CAAC;AAE3D,OAAO,EAAE,sBAAsB,EAAE,MAAM,0BAA0B,CAAC;AAElE,OAAO,EAAE,kBAAkB,EAAE,MAAM,sBAAsB,CAAC;AAE1D,MAAM,iBAAiB,GAAG,aAAa,CAA2B,IAAI,CAAC,CAAC;AAExE,MAAM,UAAU,kBAAkB,CAAC,EAAE,QAAQ,EAAiC;IAC5E,MAAM,KAAK,GAAG,kBAAkB,EAAE,CAAC;IAEnC,MAAM,KAAK,GAAG,OAAO,CACnB,GAAG,EAAE,CAAC,CAAC;QACL,UAAU,EAAE,sBAAsB,CAAC,KAAK,CAAC;QACzC,KAAK;KACN,CAAC,EACF,CAAC,KAAK,CAAC,CACR,CAAC;IAEF,OAAO,CAAC,iBAAiB,CAAC,KAAK,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,QAAQ,CAAC,EAAE,iBAAiB,CAAC,CAAC;AACzE,CAAC;AAED,MAAM,UAAU,oBAAoB;IAClC,MAAM,OAAO,GAAG,GAAG,CAAC,iBAAiB,CAAC,CAAC;IAEvC,IAAI,CAAC,OAAO,EAAE,CAAC;QACb,MAAM,IAAI,KAAK,CAAC,+DAA+D,CAAC,CAAC;IACnF,CAAC;IAED,OAAO,OAAO,CAAC;AACjB,CAAC","sourcesContent":["import React, { createContext, use, useMemo } from 'react';\n\nimport { getBreakpointFromWidth } from './getBreakpointFromWidth';\nimport type { ResponsiveRuntime } from './types';\nimport { useResponsiveWidth } from './useResponsiveWidth';\n\nconst ResponsiveContext = createContext<ResponsiveRuntime | null>(null);\n\nexport function ResponsiveProvider({ children }: { children: React.ReactNode }) {\n const width = useResponsiveWidth();\n\n const value = useMemo<ResponsiveRuntime>(\n () => ({\n breakpoint: getBreakpointFromWidth(width),\n width,\n }),\n [width],\n );\n\n return <ResponsiveContext value={value}>{children}</ResponsiveContext>;\n}\n\nexport function useResponsiveRuntime(): ResponsiveRuntime {\n const runtime = use(ResponsiveContext);\n\n if (!runtime) {\n throw new Error('useResponsiveRuntime must be used within a ResponsiveProvider');\n }\n\n return runtime;\n}\n"]}
@@ -1,3 +1,4 @@
1
1
  import type { Breakpoint } from './types';
2
+ /*** Returns the hydration-stable responsive breakpoint without requiring a provider. */
2
3
  export declare function useBreakpoint(): Breakpoint;
3
4
  //# sourceMappingURL=useBreakpoint.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"useBreakpoint.d.ts","sourceRoot":"","sources":["../../../src/core/responsive/useBreakpoint.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AAE1C,wBAAgB,aAAa,IAAI,UAAU,CAG1C"}
1
+ {"version":3,"file":"useBreakpoint.d.ts","sourceRoot":"","sources":["../../../src/core/responsive/useBreakpoint.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AAG1C,wFAAwF;AACxF,wBAAgB,aAAa,IAAI,UAAU,CAG1C"}
@@ -1,7 +1,8 @@
1
- import { useWindowDimensions } from 'react-native';
2
1
  import { getBreakpointFromWidth } from './getBreakpointFromWidth';
2
+ import { useResponsiveWidth } from './useResponsiveWidth';
3
+ /*** Returns the hydration-stable responsive breakpoint without requiring a provider. */
3
4
  export function useBreakpoint() {
4
- const { width } = useWindowDimensions();
5
+ const width = useResponsiveWidth();
5
6
  return getBreakpointFromWidth(width);
6
7
  }
7
8
  //# sourceMappingURL=useBreakpoint.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"useBreakpoint.js","sourceRoot":"","sources":["../../../src/core/responsive/useBreakpoint.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,mBAAmB,EAAE,MAAM,cAAc,CAAC;AAEnD,OAAO,EAAE,sBAAsB,EAAE,MAAM,0BAA0B,CAAC;AAGlE,MAAM,UAAU,aAAa;IAC3B,MAAM,EAAE,KAAK,EAAE,GAAG,mBAAmB,EAAE,CAAC;IACxC,OAAO,sBAAsB,CAAC,KAAK,CAAC,CAAC;AACvC,CAAC","sourcesContent":["import { useWindowDimensions } from 'react-native';\n\nimport { getBreakpointFromWidth } from './getBreakpointFromWidth';\nimport type { Breakpoint } from './types';\n\nexport function useBreakpoint(): Breakpoint {\n const { width } = useWindowDimensions();\n return getBreakpointFromWidth(width);\n}\n"]}
1
+ {"version":3,"file":"useBreakpoint.js","sourceRoot":"","sources":["../../../src/core/responsive/useBreakpoint.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,sBAAsB,EAAE,MAAM,0BAA0B,CAAC;AAElE,OAAO,EAAE,kBAAkB,EAAE,MAAM,sBAAsB,CAAC;AAE1D,wFAAwF;AACxF,MAAM,UAAU,aAAa;IAC3B,MAAM,KAAK,GAAG,kBAAkB,EAAE,CAAC;IACnC,OAAO,sBAAsB,CAAC,KAAK,CAAC,CAAC;AACvC,CAAC","sourcesContent":["import { getBreakpointFromWidth } from './getBreakpointFromWidth';\nimport type { Breakpoint } from './types';\nimport { useResponsiveWidth } from './useResponsiveWidth';\n\n/*** Returns the hydration-stable responsive breakpoint without requiring a provider. */\nexport function useBreakpoint(): Breakpoint {\n const width = useResponsiveWidth();\n return getBreakpointFromWidth(width);\n}\n"]}
@@ -0,0 +1,3 @@
1
+ /*** Reads the viewport width without changing the server and hydration render. */
2
+ export declare function useResponsiveWidth(): number;
3
+ //# sourceMappingURL=useResponsiveWidth.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useResponsiveWidth.d.ts","sourceRoot":"","sources":["../../../src/core/responsive/useResponsiveWidth.ts"],"names":[],"mappings":"AAKA,kFAAkF;AAClF,wBAAgB,kBAAkB,IAAI,MAAM,CAE3C"}
@@ -0,0 +1,21 @@
1
+ import { useSyncExternalStore } from 'react';
2
+ import { Dimensions } from 'react-native';
3
+ const serverWidth = 0;
4
+ /*** Reads the viewport width without changing the server and hydration render. */
5
+ export function useResponsiveWidth() {
6
+ return useSyncExternalStore(subscribeToWindowDimensions, getWindowWidth, getServerWidth);
7
+ }
8
+ /*** Subscribes to viewport changes after hydration. */
9
+ function subscribeToWindowDimensions(onStoreChange) {
10
+ const subscription = Dimensions.addEventListener('change', onStoreChange);
11
+ return () => subscription.remove();
12
+ }
13
+ /*** Returns the current native or browser viewport width. */
14
+ function getWindowWidth() {
15
+ return Dimensions.get('window').width;
16
+ }
17
+ /*** Supplies the deterministic responsive width used by SSR and first hydration. */
18
+ function getServerWidth() {
19
+ return serverWidth;
20
+ }
21
+ //# sourceMappingURL=useResponsiveWidth.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useResponsiveWidth.js","sourceRoot":"","sources":["../../../src/core/responsive/useResponsiveWidth.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,oBAAoB,EAAE,MAAM,OAAO,CAAC;AAC7C,OAAO,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAE1C,MAAM,WAAW,GAAG,CAAC,CAAC;AAEtB,kFAAkF;AAClF,MAAM,UAAU,kBAAkB;IAChC,OAAO,oBAAoB,CAAC,2BAA2B,EAAE,cAAc,EAAE,cAAc,CAAC,CAAC;AAC3F,CAAC;AAED,uDAAuD;AACvD,SAAS,2BAA2B,CAAC,aAAyB;IAC5D,MAAM,YAAY,GAAG,UAAU,CAAC,gBAAgB,CAAC,QAAQ,EAAE,aAAa,CAAC,CAAC;IAC1E,OAAO,GAAG,EAAE,CAAC,YAAY,CAAC,MAAM,EAAE,CAAC;AACrC,CAAC;AAED,6DAA6D;AAC7D,SAAS,cAAc;IACrB,OAAO,UAAU,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC,KAAK,CAAC;AACxC,CAAC;AAED,oFAAoF;AACpF,SAAS,cAAc;IACrB,OAAO,WAAW,CAAC;AACrB,CAAC","sourcesContent":["import { useSyncExternalStore } from 'react';\nimport { Dimensions } from 'react-native';\n\nconst serverWidth = 0;\n\n/*** Reads the viewport width without changing the server and hydration render. */\nexport function useResponsiveWidth(): number {\n return useSyncExternalStore(subscribeToWindowDimensions, getWindowWidth, getServerWidth);\n}\n\n/*** Subscribes to viewport changes after hydration. */\nfunction subscribeToWindowDimensions(onStoreChange: () => void): () => void {\n const subscription = Dimensions.addEventListener('change', onStoreChange);\n return () => subscription.remove();\n}\n\n/*** Returns the current native or browser viewport width. */\nfunction getWindowWidth(): number {\n return Dimensions.get('window').width;\n}\n\n/*** Supplies the deterministic responsive width used by SSR and first hydration. */\nfunction getServerWidth(): number {\n return serverWidth;\n}\n"]}
@@ -1 +1 @@
1
- {"version":3,"file":"ScrollView.d.ts","sourceRoot":"","sources":["../../../../../src/features/layout/adapters/inbound/ScrollView.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAC;AAI1B,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,0BAA0B,CAAC;AAIhE,uFAAuF;AACvF,wBAAgB,UAAU,CAAC,KAAK,EAAE,eAAe,qBAwBhD"}
1
+ {"version":3,"file":"ScrollView.d.ts","sourceRoot":"","sources":["../../../../../src/features/layout/adapters/inbound/ScrollView.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAC;AAQ1B,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,0BAA0B,CAAC;AAIhE,uFAAuF;AACvF,wBAAgB,UAAU,CAAC,KAAK,EAAE,eAAe,qBAiChD"}
@@ -1,5 +1,5 @@
1
1
  import React from 'react';
2
- import { ScrollView as ReactNativeScrollView } from 'react-native';
2
+ import { RefreshControl, ScrollView as ReactNativeScrollView, } from 'react-native';
3
3
  import { useResponsiveRuntime } from '../../../../core/responsive';
4
4
  import { resolveViewStyles } from '../../../../utils/resolveViewStyles';
5
5
  import { useTheme } from '../../../theme/runtime';
@@ -11,17 +11,23 @@ export function ScrollView(props) {
11
11
  const visual = splitVisualProps(spacing.rest);
12
12
  const dimensions = splitDimensionProps(visual.rest);
13
13
  const layout = splitLayoutProps(dimensions.rest);
14
- const { children, contentContainerStyle, style, ...scrollProps } = layout.rest;
14
+ const { children, contentContainerStyle, style, refreshing, onRefresh, refreshControl: nativeRefreshControl, ...scrollProps } = layout.rest;
15
15
  const resolved = resolveViewStyles(theme, breakpoint, {
16
16
  ...spacing.styleProps,
17
17
  ...visual.styleProps,
18
18
  ...dimensions.styleProps,
19
19
  ...layout.styleProps,
20
20
  });
21
- return (<ReactNativeScrollView {...scrollProps} contentContainerStyle={contentContainerStyle} style={[resolved, style]}>
21
+ return (<ReactNativeScrollView {...scrollProps} contentContainerStyle={contentContainerStyle} refreshControl={resolveRefreshControl({ nativeRefreshControl, onRefresh, refreshing })} style={[resolved, style]}>
22
22
  {children}
23
23
  </ReactNativeScrollView>);
24
24
  }
25
+ /*** Creates the standard controlled React Native refresh control only when Surface refresh props are supplied. */
26
+ function resolveRefreshControl({ nativeRefreshControl, onRefresh, refreshing, }) {
27
+ if (onRefresh === undefined && refreshing === undefined)
28
+ return nativeRefreshControl;
29
+ return <RefreshControl onRefresh={onRefresh} refreshing={refreshing ?? false}/>;
30
+ }
25
31
  /*** Splits spacing-aware Surface props from native ScrollView props. */
26
32
  function splitSpacingProps(props) {
27
33
  const { p, px, py, pt, pb, pl, pr, m, mx, my, mt, mb, ml, mr, gap, rowGap, columnGap, ...rest } = props;
@@ -1 +1 @@
1
- {"version":3,"file":"ScrollView.js","sourceRoot":"","sources":["../../../../../src/features/layout/adapters/inbound/ScrollView.tsx"],"names":[],"mappings":"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","sourcesContent":["import React from 'react';\nimport { ScrollView as ReactNativeScrollView } from 'react-native';\n\nimport { useResponsiveRuntime } from '../../../../core/responsive';\nimport type { ScrollViewProps } from '../../../../types/layout';\nimport { resolveViewStyles } from '../../../../utils/resolveViewStyles';\nimport { useTheme } from '../../../theme/runtime';\n\n/*** Renders the token-aware responsive Surface adapter for React Native ScrollView. */\nexport function ScrollView(props: ScrollViewProps) {\n const { theme } = useTheme();\n const { breakpoint } = useResponsiveRuntime();\n const spacing = splitSpacingProps(props);\n const visual = splitVisualProps(spacing.rest);\n const dimensions = splitDimensionProps(visual.rest);\n const layout = splitLayoutProps(dimensions.rest);\n const { children, contentContainerStyle, style, ...scrollProps } = layout.rest;\n const resolved = resolveViewStyles(theme, breakpoint, {\n ...spacing.styleProps,\n ...visual.styleProps,\n ...dimensions.styleProps,\n ...layout.styleProps,\n });\n\n return (\n <ReactNativeScrollView\n {...scrollProps}\n contentContainerStyle={contentContainerStyle}\n style={[resolved, style]}\n >\n {children}\n </ReactNativeScrollView>\n );\n}\n\n/*** Splits spacing-aware Surface props from native ScrollView props. */\nfunction splitSpacingProps(props: ScrollViewProps) {\n const { p, px, py, pt, pb, pl, pr, m, mx, my, mt, mb, ml, mr, gap, rowGap, columnGap, ...rest } =\n props;\n return {\n rest,\n styleProps: { p, px, py, pt, pb, pl, pr, m, mx, my, mt, mb, ml, mr, gap, rowGap, columnGap },\n };\n}\n\n/*** Splits token-aware visual Surface props from native ScrollView props. */\nfunction splitVisualProps(props: ReturnType<typeof splitSpacingProps>['rest']) {\n const { bg, radius, borderWidth, borderColor, ...rest } = props;\n return { rest, styleProps: { bg, radius, borderWidth, borderColor } };\n}\n\n/*** Splits responsive dimension Surface props from native ScrollView props. */\nfunction splitDimensionProps(props: ReturnType<typeof splitVisualProps>['rest']) {\n const { width, height, minWidth, maxWidth, minHeight, maxHeight, ...rest } = props;\n return { rest, styleProps: { width, height, minWidth, maxWidth, minHeight, maxHeight } };\n}\n\n/*** Splits responsive flex and positioning Surface props from native ScrollView props. */\nfunction splitLayoutProps(props: ReturnType<typeof splitDimensionProps>['rest']) {\n const {\n flex,\n flexGrow,\n flexShrink,\n flexBasis,\n direction,\n align,\n justify,\n wrap,\n alignSelf,\n position,\n top,\n bottom,\n left,\n right,\n overflow,\n zIndex,\n opacity,\n ...rest\n } = props;\n return {\n rest,\n styleProps: {\n flex,\n flexGrow,\n flexShrink,\n flexBasis,\n direction,\n align,\n justify,\n wrap,\n alignSelf,\n position,\n top,\n bottom,\n left,\n right,\n overflow,\n zIndex,\n opacity,\n },\n };\n}\n"]}
1
+ {"version":3,"file":"ScrollView.js","sourceRoot":"","sources":["../../../../../src/features/layout/adapters/inbound/ScrollView.tsx"],"names":[],"mappings":"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","sourcesContent":["import React from 'react';\nimport {\n RefreshControl,\n ScrollView as ReactNativeScrollView,\n type ScrollViewProps as ReactNativeScrollViewProps,\n} from 'react-native';\n\nimport { useResponsiveRuntime } from '../../../../core/responsive';\nimport type { ScrollViewProps } from '../../../../types/layout';\nimport { resolveViewStyles } from '../../../../utils/resolveViewStyles';\nimport { useTheme } from '../../../theme/runtime';\n\n/*** Renders the token-aware responsive Surface adapter for React Native ScrollView. */\nexport function ScrollView(props: ScrollViewProps) {\n const { theme } = useTheme();\n const { breakpoint } = useResponsiveRuntime();\n const spacing = splitSpacingProps(props);\n const visual = splitVisualProps(spacing.rest);\n const dimensions = splitDimensionProps(visual.rest);\n const layout = splitLayoutProps(dimensions.rest);\n const {\n children,\n contentContainerStyle,\n style,\n refreshing,\n onRefresh,\n refreshControl: nativeRefreshControl,\n ...scrollProps\n } = layout.rest;\n const resolved = resolveViewStyles(theme, breakpoint, {\n ...spacing.styleProps,\n ...visual.styleProps,\n ...dimensions.styleProps,\n ...layout.styleProps,\n });\n\n return (\n <ReactNativeScrollView\n {...scrollProps}\n contentContainerStyle={contentContainerStyle}\n refreshControl={resolveRefreshControl({ nativeRefreshControl, onRefresh, refreshing })}\n style={[resolved, style]}\n >\n {children}\n </ReactNativeScrollView>\n );\n}\n\n/*** Creates the standard controlled React Native refresh control only when Surface refresh props are supplied. */\nfunction resolveRefreshControl({\n nativeRefreshControl,\n onRefresh,\n refreshing,\n}: Pick<ScrollViewProps, 'onRefresh' | 'refreshing'> & {\n nativeRefreshControl: ReactNativeScrollViewProps['refreshControl'];\n}) {\n if (onRefresh === undefined && refreshing === undefined) return nativeRefreshControl;\n\n return <RefreshControl onRefresh={onRefresh} refreshing={refreshing ?? false} />;\n}\n\n/*** Splits spacing-aware Surface props from native ScrollView props. */\nfunction splitSpacingProps(props: ScrollViewProps) {\n const { p, px, py, pt, pb, pl, pr, m, mx, my, mt, mb, ml, mr, gap, rowGap, columnGap, ...rest } =\n props;\n return {\n rest,\n styleProps: { p, px, py, pt, pb, pl, pr, m, mx, my, mt, mb, ml, mr, gap, rowGap, columnGap },\n };\n}\n\n/*** Splits token-aware visual Surface props from native ScrollView props. */\nfunction splitVisualProps(props: ReturnType<typeof splitSpacingProps>['rest']) {\n const { bg, radius, borderWidth, borderColor, ...rest } = props;\n return { rest, styleProps: { bg, radius, borderWidth, borderColor } };\n}\n\n/*** Splits responsive dimension Surface props from native ScrollView props. */\nfunction splitDimensionProps(props: ReturnType<typeof splitVisualProps>['rest']) {\n const { width, height, minWidth, maxWidth, minHeight, maxHeight, ...rest } = props;\n return { rest, styleProps: { width, height, minWidth, maxWidth, minHeight, maxHeight } };\n}\n\n/*** Splits responsive flex and positioning Surface props from native ScrollView props. */\nfunction splitLayoutProps(props: ReturnType<typeof splitDimensionProps>['rest']) {\n const {\n flex,\n flexGrow,\n flexShrink,\n flexBasis,\n direction,\n align,\n justify,\n wrap,\n alignSelf,\n position,\n top,\n bottom,\n left,\n right,\n overflow,\n zIndex,\n opacity,\n ...rest\n } = props;\n return {\n rest,\n styleProps: {\n flex,\n flexGrow,\n flexShrink,\n flexBasis,\n direction,\n align,\n justify,\n wrap,\n alignSelf,\n position,\n top,\n bottom,\n left,\n right,\n overflow,\n zIndex,\n opacity,\n },\n };\n}\n"]}
@@ -82,6 +82,10 @@ export interface GridProps extends Omit<ViewProps, 'children'> {
82
82
  export interface ScrollViewProps extends ViewStyleProps, Omit<ReactNativeScrollViewProps, 'children' | 'contentContainerStyle' | 'style'> {
83
83
  children?: React.ReactNode;
84
84
  contentContainerStyle?: StyleProp<ViewStyle>;
85
+ /** Controlled visibility of the native refresh indicator. */
86
+ refreshing?: boolean;
87
+ /** Called by the native refresh control after a pull-to-refresh gesture. */
88
+ onRefresh?: () => void;
85
89
  }
86
90
  export {};
87
91
  //# sourceMappingURL=layout.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"layout.d.ts","sourceRoot":"","sources":["../../src/types/layout.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"layout.d.ts","sourceRoot":"","sources":["../../src/types/layout.ts"],"names":[],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"layout.js","sourceRoot":"","sources":["../../src/types/layout.ts"],"names":[],"mappings":"","sourcesContent":["import type React from 'react';\nimport type {\n ScrollViewProps as ReactNativeScrollViewProps,\n StyleProp,\n ViewProps as ReactNativeViewProps,\n ViewStyle,\n} from 'react-native';\n\nimport type { Responsive } from '../core/responsive';\nimport type { SurfaceTheme } from './theme';\n\ntype SpaceToken = keyof SurfaceTheme['spacing'];\ntype RadiusToken = keyof SurfaceTheme['radii'];\ntype ColorToken = keyof SurfaceTheme['colors'];\ntype RadiusValue = number | RadiusToken;\n\nexport type SpaceValue = number | SpaceToken;\nexport type ColorValue = string | ColorToken;\n\nexport interface ViewStyleProps {\n p?: Responsive<SpaceValue>;\n px?: Responsive<SpaceValue>;\n py?: Responsive<SpaceValue>;\n pt?: Responsive<SpaceValue>;\n pb?: Responsive<SpaceValue>;\n pl?: Responsive<SpaceValue>;\n pr?: Responsive<SpaceValue>;\n m?: Responsive<SpaceValue>;\n mx?: Responsive<SpaceValue>;\n my?: Responsive<SpaceValue>;\n mt?: Responsive<SpaceValue>;\n mb?: Responsive<SpaceValue>;\n ml?: Responsive<SpaceValue>;\n mr?: Responsive<SpaceValue>;\n gap?: Responsive<SpaceValue>;\n rowGap?: Responsive<SpaceValue>;\n columnGap?: Responsive<SpaceValue>;\n bg?: Responsive<ColorValue>;\n radius?: Responsive<RadiusValue>;\n borderWidth?: Responsive<number>;\n borderColor?: Responsive<ColorValue>;\n width?: Responsive<number | string>;\n height?: Responsive<number | string>;\n minWidth?: Responsive<number | string>;\n maxWidth?: Responsive<number | string>;\n minHeight?: Responsive<number | string>;\n maxHeight?: Responsive<number | string>;\n flex?: Responsive<number>;\n flexGrow?: Responsive<number>;\n flexShrink?: Responsive<number>;\n flexBasis?: Responsive<number | string>;\n direction?: Responsive<'row' | 'column'>;\n align?: Responsive<'flex-start' | 'center' | 'flex-end' | 'stretch' | 'baseline'>;\n justify?: Responsive<\n 'flex-start' | 'center' | 'flex-end' | 'space-between' | 'space-around' | 'space-evenly'\n >;\n wrap?: Responsive<'nowrap' | 'wrap'>;\n alignSelf?: Responsive<ViewStyle['alignSelf']>;\n position?: Responsive<ViewStyle['position']>;\n top?: Responsive<number>;\n bottom?: Responsive<number>;\n left?: Responsive<number>;\n right?: Responsive<number>;\n overflow?: Responsive<ViewStyle['overflow']>;\n zIndex?: Responsive<number>;\n opacity?: Responsive<number>;\n style?: StyleProp<ViewStyle>;\n}\n\nexport interface ViewProps extends ViewStyleProps {\n accessibilityLabel?: ReactNativeViewProps['accessibilityLabel'];\n accessibilityLabelledBy?: ReactNativeViewProps['accessibilityLabelledBy'];\n accessibilityRole?: ReactNativeViewProps['accessibilityRole'];\n accessibilityState?: ReactNativeViewProps['accessibilityState'];\n accessible?: ReactNativeViewProps['accessible'];\n children?: React.ReactNode;\n nativeID?: ReactNativeViewProps['nativeID'];\n pointerEvents?: ReactNativeViewProps['pointerEvents'];\n testID?: string;\n}\n\nexport interface DividerProps extends Omit<ViewProps, 'bg' | 'height' | 'width'> {\n orientation?: 'horizontal' | 'vertical';\n color?: ColorValue;\n thickness?: number;\n}\n\nexport interface GridProps extends Omit<ViewProps, 'children'> {\n children?: React.ReactNode;\n cols?: Responsive<number>;\n gap?: Responsive<SpaceValue>;\n rowGap?: Responsive<SpaceValue>;\n colGap?: Responsive<SpaceValue>;\n minItemWidth?: Responsive<number>;\n}\n\nexport interface ScrollViewProps\n extends\n ViewStyleProps,\n Omit<ReactNativeScrollViewProps, 'children' | 'contentContainerStyle' | 'style'> {\n children?: React.ReactNode;\n contentContainerStyle?: StyleProp<ViewStyle>;\n}\n"]}
1
+ {"version":3,"file":"layout.js","sourceRoot":"","sources":["../../src/types/layout.ts"],"names":[],"mappings":"","sourcesContent":["import type React from 'react';\nimport type {\n ScrollViewProps as ReactNativeScrollViewProps,\n StyleProp,\n ViewProps as ReactNativeViewProps,\n ViewStyle,\n} from 'react-native';\n\nimport type { Responsive } from '../core/responsive';\nimport type { SurfaceTheme } from './theme';\n\ntype SpaceToken = keyof SurfaceTheme['spacing'];\ntype RadiusToken = keyof SurfaceTheme['radii'];\ntype ColorToken = keyof SurfaceTheme['colors'];\ntype RadiusValue = number | RadiusToken;\n\nexport type SpaceValue = number | SpaceToken;\nexport type ColorValue = string | ColorToken;\n\nexport interface ViewStyleProps {\n p?: Responsive<SpaceValue>;\n px?: Responsive<SpaceValue>;\n py?: Responsive<SpaceValue>;\n pt?: Responsive<SpaceValue>;\n pb?: Responsive<SpaceValue>;\n pl?: Responsive<SpaceValue>;\n pr?: Responsive<SpaceValue>;\n m?: Responsive<SpaceValue>;\n mx?: Responsive<SpaceValue>;\n my?: Responsive<SpaceValue>;\n mt?: Responsive<SpaceValue>;\n mb?: Responsive<SpaceValue>;\n ml?: Responsive<SpaceValue>;\n mr?: Responsive<SpaceValue>;\n gap?: Responsive<SpaceValue>;\n rowGap?: Responsive<SpaceValue>;\n columnGap?: Responsive<SpaceValue>;\n bg?: Responsive<ColorValue>;\n radius?: Responsive<RadiusValue>;\n borderWidth?: Responsive<number>;\n borderColor?: Responsive<ColorValue>;\n width?: Responsive<number | string>;\n height?: Responsive<number | string>;\n minWidth?: Responsive<number | string>;\n maxWidth?: Responsive<number | string>;\n minHeight?: Responsive<number | string>;\n maxHeight?: Responsive<number | string>;\n flex?: Responsive<number>;\n flexGrow?: Responsive<number>;\n flexShrink?: Responsive<number>;\n flexBasis?: Responsive<number | string>;\n direction?: Responsive<'row' | 'column'>;\n align?: Responsive<'flex-start' | 'center' | 'flex-end' | 'stretch' | 'baseline'>;\n justify?: Responsive<\n 'flex-start' | 'center' | 'flex-end' | 'space-between' | 'space-around' | 'space-evenly'\n >;\n wrap?: Responsive<'nowrap' | 'wrap'>;\n alignSelf?: Responsive<ViewStyle['alignSelf']>;\n position?: Responsive<ViewStyle['position']>;\n top?: Responsive<number>;\n bottom?: Responsive<number>;\n left?: Responsive<number>;\n right?: Responsive<number>;\n overflow?: Responsive<ViewStyle['overflow']>;\n zIndex?: Responsive<number>;\n opacity?: Responsive<number>;\n style?: StyleProp<ViewStyle>;\n}\n\nexport interface ViewProps extends ViewStyleProps {\n accessibilityLabel?: ReactNativeViewProps['accessibilityLabel'];\n accessibilityLabelledBy?: ReactNativeViewProps['accessibilityLabelledBy'];\n accessibilityRole?: ReactNativeViewProps['accessibilityRole'];\n accessibilityState?: ReactNativeViewProps['accessibilityState'];\n accessible?: ReactNativeViewProps['accessible'];\n children?: React.ReactNode;\n nativeID?: ReactNativeViewProps['nativeID'];\n pointerEvents?: ReactNativeViewProps['pointerEvents'];\n testID?: string;\n}\n\nexport interface DividerProps extends Omit<ViewProps, 'bg' | 'height' | 'width'> {\n orientation?: 'horizontal' | 'vertical';\n color?: ColorValue;\n thickness?: number;\n}\n\nexport interface GridProps extends Omit<ViewProps, 'children'> {\n children?: React.ReactNode;\n cols?: Responsive<number>;\n gap?: Responsive<SpaceValue>;\n rowGap?: Responsive<SpaceValue>;\n colGap?: Responsive<SpaceValue>;\n minItemWidth?: Responsive<number>;\n}\n\nexport interface ScrollViewProps\n extends\n ViewStyleProps,\n Omit<ReactNativeScrollViewProps, 'children' | 'contentContainerStyle' | 'style'> {\n children?: React.ReactNode;\n contentContainerStyle?: StyleProp<ViewStyle>;\n /** Controlled visibility of the native refresh indicator. */\n refreshing?: boolean;\n /** Called by the native refresh control after a pull-to-refresh gesture. */\n onRefresh?: () => void;\n}\n"]}
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@ankhorage/surface",
3
3
  "type": "module",
4
- "version": "9.4.10",
4
+ "version": "9.4.12",
5
5
  "description": "Stable UI foundation for React Native and React Native Web.",
6
6
  "homepage": "https://github.com/ankhorage/surface#readme",
7
7
  "bugs": {
@@ -113,7 +113,7 @@
113
113
  },
114
114
  "devDependencies": {
115
115
  "@ankhorage/capability": "^0.2.2",
116
- "@ankhorage/devtools": "^2.3.23",
116
+ "@ankhorage/devtools": "^2.3.24",
117
117
  "@ankhorage/paradox": "^0.3.5",
118
118
  "@react-native-vector-icons/fontawesome": "^13.1.5",
119
119
  "@react-native-vector-icons/fontawesome5": "^13.1.5",
@@ -1,13 +1,13 @@
1
1
  import React, { createContext, use, useMemo } from 'react';
2
- import { useWindowDimensions } from 'react-native';
3
2
 
4
3
  import { getBreakpointFromWidth } from './getBreakpointFromWidth';
5
4
  import type { ResponsiveRuntime } from './types';
5
+ import { useResponsiveWidth } from './useResponsiveWidth';
6
6
 
7
7
  const ResponsiveContext = createContext<ResponsiveRuntime | null>(null);
8
8
 
9
9
  export function ResponsiveProvider({ children }: { children: React.ReactNode }) {
10
- const { width } = useWindowDimensions();
10
+ const width = useResponsiveWidth();
11
11
 
12
12
  const value = useMemo<ResponsiveRuntime>(
13
13
  () => ({
@@ -1,9 +1,9 @@
1
- import { useWindowDimensions } from 'react-native';
2
-
3
1
  import { getBreakpointFromWidth } from './getBreakpointFromWidth';
4
2
  import type { Breakpoint } from './types';
3
+ import { useResponsiveWidth } from './useResponsiveWidth';
5
4
 
5
+ /*** Returns the hydration-stable responsive breakpoint without requiring a provider. */
6
6
  export function useBreakpoint(): Breakpoint {
7
- const { width } = useWindowDimensions();
7
+ const width = useResponsiveWidth();
8
8
  return getBreakpointFromWidth(width);
9
9
  }
@@ -0,0 +1,25 @@
1
+ import { useSyncExternalStore } from 'react';
2
+ import { Dimensions } from 'react-native';
3
+
4
+ const serverWidth = 0;
5
+
6
+ /*** Reads the viewport width without changing the server and hydration render. */
7
+ export function useResponsiveWidth(): number {
8
+ return useSyncExternalStore(subscribeToWindowDimensions, getWindowWidth, getServerWidth);
9
+ }
10
+
11
+ /*** Subscribes to viewport changes after hydration. */
12
+ function subscribeToWindowDimensions(onStoreChange: () => void): () => void {
13
+ const subscription = Dimensions.addEventListener('change', onStoreChange);
14
+ return () => subscription.remove();
15
+ }
16
+
17
+ /*** Returns the current native or browser viewport width. */
18
+ function getWindowWidth(): number {
19
+ return Dimensions.get('window').width;
20
+ }
21
+
22
+ /*** Supplies the deterministic responsive width used by SSR and first hydration. */
23
+ function getServerWidth(): number {
24
+ return serverWidth;
25
+ }
@@ -0,0 +1,60 @@
1
+ import { expect, mock, test } from 'bun:test';
2
+ import React from 'react';
3
+ import { renderToString } from 'react-dom/server';
4
+ import * as ReactNativeWeb from 'react-native-web';
5
+
6
+ type RecordedScrollViewProps = Readonly<Record<string, unknown>>;
7
+
8
+ const recordedScrollViewProps: RecordedScrollViewProps[] = [];
9
+
10
+ await mock.module('react-native', () => ({
11
+ ...ReactNativeWeb,
12
+ RefreshControl: () => null,
13
+ ScrollView: (props: RecordedScrollViewProps) => {
14
+ recordedScrollViewProps.push(props);
15
+ return null;
16
+ },
17
+ }));
18
+
19
+ const { ScrollView } = await import('./ScrollView');
20
+ const { ThemeProvider } = await import('../../../theme/runtime');
21
+
22
+ /*** Renders Surface ScrollView under the production theme boundary and captures native props. */
23
+ function renderScrollView(props: React.ComponentProps<typeof ScrollView>) {
24
+ recordedScrollViewProps.splice(0);
25
+ renderToString(
26
+ <ThemeProvider>
27
+ <ScrollView {...props} />
28
+ </ThemeProvider>,
29
+ );
30
+
31
+ const renderedProps = recordedScrollViewProps.at(-1);
32
+ if (renderedProps === undefined) throw new Error('ScrollView did not render.');
33
+ return renderedProps;
34
+ }
35
+
36
+ test('creates a controlled native RefreshControl from Surface refresh props', () => {
37
+ const onRefresh = () => undefined;
38
+ const rendered = renderScrollView({ onRefresh, refreshing: true });
39
+ const refreshControl = rendered.refreshControl as React.ReactElement<{
40
+ onRefresh?: () => void;
41
+ refreshing: boolean;
42
+ }>;
43
+
44
+ expect(refreshControl.type).toBeDefined();
45
+ expect(refreshControl.props).toEqual({ onRefresh, refreshing: true });
46
+ });
47
+
48
+ test('defaults a requested refresh control to its inactive controlled state', () => {
49
+ const rendered = renderScrollView({ onRefresh: () => undefined });
50
+ const refreshControl = rendered.refreshControl as React.ReactElement<{ refreshing: boolean }>;
51
+
52
+ expect(refreshControl.props.refreshing).toBe(false);
53
+ });
54
+
55
+ test('preserves a caller-provided native refresh control when Surface refresh props are absent', () => {
56
+ const nativeRefreshControl = <ReactNativeWeb.View />;
57
+ const rendered = renderScrollView({ refreshControl: nativeRefreshControl });
58
+
59
+ expect(rendered.refreshControl).toBe(nativeRefreshControl);
60
+ });
@@ -1,5 +1,9 @@
1
1
  import React from 'react';
2
- import { ScrollView as ReactNativeScrollView } from 'react-native';
2
+ import {
3
+ RefreshControl,
4
+ ScrollView as ReactNativeScrollView,
5
+ type ScrollViewProps as ReactNativeScrollViewProps,
6
+ } from 'react-native';
3
7
 
4
8
  import { useResponsiveRuntime } from '../../../../core/responsive';
5
9
  import type { ScrollViewProps } from '../../../../types/layout';
@@ -14,7 +18,15 @@ export function ScrollView(props: ScrollViewProps) {
14
18
  const visual = splitVisualProps(spacing.rest);
15
19
  const dimensions = splitDimensionProps(visual.rest);
16
20
  const layout = splitLayoutProps(dimensions.rest);
17
- const { children, contentContainerStyle, style, ...scrollProps } = layout.rest;
21
+ const {
22
+ children,
23
+ contentContainerStyle,
24
+ style,
25
+ refreshing,
26
+ onRefresh,
27
+ refreshControl: nativeRefreshControl,
28
+ ...scrollProps
29
+ } = layout.rest;
18
30
  const resolved = resolveViewStyles(theme, breakpoint, {
19
31
  ...spacing.styleProps,
20
32
  ...visual.styleProps,
@@ -26,6 +38,7 @@ export function ScrollView(props: ScrollViewProps) {
26
38
  <ReactNativeScrollView
27
39
  {...scrollProps}
28
40
  contentContainerStyle={contentContainerStyle}
41
+ refreshControl={resolveRefreshControl({ nativeRefreshControl, onRefresh, refreshing })}
29
42
  style={[resolved, style]}
30
43
  >
31
44
  {children}
@@ -33,6 +46,19 @@ export function ScrollView(props: ScrollViewProps) {
33
46
  );
34
47
  }
35
48
 
49
+ /*** Creates the standard controlled React Native refresh control only when Surface refresh props are supplied. */
50
+ function resolveRefreshControl({
51
+ nativeRefreshControl,
52
+ onRefresh,
53
+ refreshing,
54
+ }: Pick<ScrollViewProps, 'onRefresh' | 'refreshing'> & {
55
+ nativeRefreshControl: ReactNativeScrollViewProps['refreshControl'];
56
+ }) {
57
+ if (onRefresh === undefined && refreshing === undefined) return nativeRefreshControl;
58
+
59
+ return <RefreshControl onRefresh={onRefresh} refreshing={refreshing ?? false} />;
60
+ }
61
+
36
62
  /*** Splits spacing-aware Surface props from native ScrollView props. */
37
63
  function splitSpacingProps(props: ScrollViewProps) {
38
64
  const { p, px, py, pt, pb, pl, pr, m, mx, my, mt, mb, ml, mr, gap, rowGap, columnGap, ...rest } =
@@ -100,4 +100,8 @@ export interface ScrollViewProps
100
100
  Omit<ReactNativeScrollViewProps, 'children' | 'contentContainerStyle' | 'style'> {
101
101
  children?: React.ReactNode;
102
102
  contentContainerStyle?: StyleProp<ViewStyle>;
103
+ /** Controlled visibility of the native refresh indicator. */
104
+ refreshing?: boolean;
105
+ /** Called by the native refresh control after a pull-to-refresh gesture. */
106
+ onRefresh?: () => void;
103
107
  }
@@ -2,7 +2,18 @@ import { expect, mock, test } from 'bun:test';
2
2
  import { Window } from 'happy-dom';
3
3
  import React, { act } from 'react';
4
4
  import { renderToString } from 'react-dom/server';
5
- import * as ReactNativeWeb from 'react-native-web';
5
+
6
+ const browserWindow = new Window({ url: 'https://surface.test/' });
7
+ Object.assign(globalThis, {
8
+ IS_REACT_ACT_ENVIRONMENT: true,
9
+ Node: browserWindow.Node,
10
+ ShadowRoot: browserWindow.ShadowRoot,
11
+ document: browserWindow.document,
12
+ navigator: browserWindow.navigator,
13
+ window: browserWindow,
14
+ });
15
+
16
+ const ReactNativeWeb = await import('react-native-web');
6
17
 
7
18
  await mock.module('react-native', () => ReactNativeWeb);
8
19
 
@@ -10,7 +21,7 @@ const scrollContentStyle = { padding: 4 };
10
21
 
11
22
  const { Grid, ScrollView, View } = await import('./features/layout/public');
12
23
  const { KeyboardAvoidingView } = await import('./features/keyboard-avoiding-view/public');
13
- const { Show } = await import('./core/responsive/Show');
24
+ const { Show, useBreakpoint } = await import('./core/responsive');
14
25
  const { OverlayProvider } = await import('./internal/overlay/OverlayProvider');
15
26
  const { ThemeProvider } = await import('./features/theme/runtime');
16
27
 
@@ -24,34 +35,38 @@ function ResponsiveAcceptanceTree() {
24
35
  <ReactNativeWeb.View testID="grid-second" />
25
36
  </Grid>
26
37
  <Show
27
- fallback={<ReactNativeWeb.Text>static-fallback</ReactNativeWeb.Text>}
38
+ fallback={
39
+ <ReactNativeWeb.View testID="mobile-branch">
40
+ <ReactNativeWeb.Text>static-fallback</ReactNativeWeb.Text>
41
+ </ReactNativeWeb.View>
42
+ }
28
43
  when={{ base: false, md: true }}
29
44
  >
30
- <ReactNativeWeb.Text>wide-content</ReactNativeWeb.Text>
45
+ <ReactNativeWeb.Text testID="desktop-branch">wide-content</ReactNativeWeb.Text>
31
46
  </Show>
32
47
  </ScrollView>
33
48
  </View>
49
+ <BreakpointProbe />
34
50
  </ThemeProvider>
35
51
  );
36
52
  }
37
53
 
38
- test('RN Web 0.21 statically renders and hydrates representative responsive primitives', async () => {
54
+ function BreakpointProbe() {
55
+ return <ReactNativeWeb.Text testID="breakpoint-probe">{useBreakpoint()}</ReactNativeWeb.Text>;
56
+ }
57
+
58
+ test('RN Web 0.21 preserves SSR responsive structure during desktop hydration', async () => {
59
+ act(() => setViewportWidth(0));
39
60
  const markup = renderToString(<ResponsiveAcceptanceTree />);
40
61
 
41
62
  expect(markup).toContain('data-testid="container"');
42
63
  expect(markup).toContain('data-testid="scroll-view"');
43
64
  expect(markup).toContain('data-testid="grid"');
65
+ expect(markup).toContain('base');
44
66
  expect(markup).toContain('static-fallback');
45
67
  expect(markup).not.toContain('wide-content');
46
68
 
47
- const browserWindow = new Window({ url: 'https://surface.test/' });
48
- Object.assign(globalThis, {
49
- IS_REACT_ACT_ENVIRONMENT: true,
50
- Node: browserWindow.Node,
51
- document: browserWindow.document,
52
- navigator: browserWindow.navigator,
53
- window: browserWindow,
54
- });
69
+ act(() => setViewportWidth(1440));
55
70
  const container = browserWindow.document.createElement('div');
56
71
  container.innerHTML = markup;
57
72
  browserWindow.document.body.append(container);
@@ -64,25 +79,66 @@ test('RN Web 0.21 statically renders and hydrates representative responsive prim
64
79
  try {
65
80
  const { hydrateRoot } = await import('react-dom/client');
66
81
  const root = hydrateRoot(container as unknown as Element, <ResponsiveAcceptanceTree />);
82
+
83
+ expect(container.querySelector('[data-testid="mobile-branch"]')).not.toBeNull();
84
+ expect(container.querySelector('[data-testid="desktop-branch"]')).toBeNull();
85
+ expect(container.querySelector('[data-testid="breakpoint-probe"]')?.textContent).toBe('base');
67
86
  await act(async () => Promise.resolve());
68
87
 
69
88
  expect(container.querySelector('[data-testid="container"]')).not.toBeNull();
70
89
  expect(container.querySelector('[data-testid="scroll-view"]')).not.toBeNull();
71
90
  expect(container.querySelectorAll('[data-testid^="grid-"]')).toHaveLength(2);
91
+ expect(container.querySelector('[data-testid="mobile-branch"]')).toBeNull();
92
+ expect(container.querySelector('[data-testid="desktop-branch"]')).not.toBeNull();
93
+ expect(container.querySelector('[data-testid="breakpoint-probe"]')?.textContent).toBe('xl');
94
+ expect(hydrationErrors).toEqual([]);
95
+
96
+ act(() => root.unmount());
97
+ } finally {
98
+ console.error = originalError;
99
+ container.remove();
100
+ }
101
+ });
102
+
103
+ test('RN Web 0.21 keeps the server responsive structure at a narrow viewport', async () => {
104
+ act(() => setViewportWidth(0));
105
+ const markup = renderToString(<ResponsiveAcceptanceTree />);
106
+ act(() => setViewportWidth(375));
107
+ const container = browserWindow.document.createElement('div');
108
+ container.innerHTML = markup;
109
+ browserWindow.document.body.append(container);
110
+ const hydrationErrors: string[] = [];
111
+ const originalError = console.error;
112
+ console.error = (...values: unknown[]) => {
113
+ hydrationErrors.push(values.map(String).join(' '));
114
+ };
115
+
116
+ try {
117
+ const { hydrateRoot } = await import('react-dom/client');
118
+ const root = hydrateRoot(container as unknown as Element, <ResponsiveAcceptanceTree />);
119
+ await act(async () => Promise.resolve());
120
+
121
+ expect(container.querySelector('[data-testid="mobile-branch"]')).not.toBeNull();
122
+ expect(container.querySelector('[data-testid="desktop-branch"]')).toBeNull();
123
+ expect(container.querySelector('[data-testid="breakpoint-probe"]')?.textContent).toBe('base');
124
+ expect(container.querySelectorAll('[data-testid^="grid-"]')).toHaveLength(2);
72
125
  expect(hydrationErrors).toEqual([]);
73
126
 
74
127
  act(() => root.unmount());
75
128
  } finally {
76
129
  console.error = originalError;
77
- browserWindow.close();
78
- Reflect.deleteProperty(globalThis, 'IS_REACT_ACT_ENVIRONMENT');
79
- Reflect.deleteProperty(globalThis, 'Node');
80
- Reflect.deleteProperty(globalThis, 'document');
81
- Reflect.deleteProperty(globalThis, 'navigator');
82
- Reflect.deleteProperty(globalThis, 'window');
130
+ container.remove();
83
131
  }
84
132
  });
85
133
 
134
+ function setViewportWidth(width: number) {
135
+ Object.defineProperty(browserWindow.document.documentElement, 'clientWidth', {
136
+ configurable: true,
137
+ value: width,
138
+ });
139
+ browserWindow.dispatchEvent(new browserWindow.Event('resize'));
140
+ }
141
+
86
142
  test('RN Web 0.21 compiles box-none overlay pointer events into a hit-test-safe class', () => {
87
143
  const markup = renderToString(
88
144
  <OverlayProvider>
@@ -109,3 +165,22 @@ test('RN Web 0.21 renders keyboard-safe content through the Surface layout bound
109
165
  expect(markup).toContain('data-testid="keyboard-safe"');
110
166
  expect(markup).toContain('Keyboard-safe content');
111
167
  });
168
+
169
+ test('RN Web renders controlled refresh state without simulating a native pull gesture', () => {
170
+ const refreshSignals: undefined[] = [];
171
+ const markup = renderToString(
172
+ <ThemeProvider>
173
+ <ScrollView
174
+ onRefresh={() => refreshSignals.push(undefined)}
175
+ refreshing
176
+ testID="refreshable-scroll-view"
177
+ >
178
+ <ReactNativeWeb.Text>refreshable content</ReactNativeWeb.Text>
179
+ </ScrollView>
180
+ </ThemeProvider>,
181
+ );
182
+
183
+ expect(markup).toContain('data-testid="refreshable-scroll-view"');
184
+ expect(markup).toContain('refreshable content');
185
+ expect(refreshSignals).toEqual([]);
186
+ });