@ankhorage/surface 2.2.0 → 3.0.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 (122) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/README.md +1 -2
  3. package/dist/components/action-sheet/ActionSheet.d.ts.map +1 -1
  4. package/dist/components/action-sheet/ActionSheet.js +12 -7
  5. package/dist/components/action-sheet/ActionSheet.js.map +1 -1
  6. package/dist/components/button/Button.d.ts.map +1 -1
  7. package/dist/components/button/Button.js +5 -7
  8. package/dist/components/button/Button.js.map +1 -1
  9. package/dist/components/button/types.d.ts +2 -5
  10. package/dist/components/button/types.d.ts.map +1 -1
  11. package/dist/components/button/types.js.map +1 -1
  12. package/dist/components/drawer/Drawer.d.ts.map +1 -1
  13. package/dist/components/drawer/Drawer.js +12 -7
  14. package/dist/components/drawer/Drawer.js.map +1 -1
  15. package/dist/components/icon-button/IconButton.d.ts.map +1 -1
  16. package/dist/components/icon-button/IconButton.js +1 -1
  17. package/dist/components/icon-button/IconButton.js.map +1 -1
  18. package/dist/components/icon-button/types.d.ts +2 -5
  19. package/dist/components/icon-button/types.d.ts.map +1 -1
  20. package/dist/components/icon-button/types.js.map +1 -1
  21. package/dist/components/menu/Menu.d.ts.map +1 -1
  22. package/dist/components/menu/Menu.js +12 -7
  23. package/dist/components/menu/Menu.js.map +1 -1
  24. package/dist/components/modal/Modal.d.ts.map +1 -1
  25. package/dist/components/modal/Modal.js +12 -7
  26. package/dist/components/modal/Modal.js.map +1 -1
  27. package/dist/components/navigation/NavigationItem.d.ts.map +1 -1
  28. package/dist/components/navigation/NavigationItem.js +1 -1
  29. package/dist/components/navigation/NavigationItem.js.map +1 -1
  30. package/dist/components/navigation/TabBarItem.d.ts.map +1 -1
  31. package/dist/components/navigation/TabBarItem.js +1 -1
  32. package/dist/components/navigation/TabBarItem.js.map +1 -1
  33. package/dist/components/navigation/types.d.ts +2 -2
  34. package/dist/components/navigation/types.d.ts.map +1 -1
  35. package/dist/components/navigation/types.js.map +1 -1
  36. package/dist/components/toast/ToastProvider.d.ts.map +1 -1
  37. package/dist/components/toast/ToastProvider.js +3 -2
  38. package/dist/components/toast/ToastProvider.js.map +1 -1
  39. package/dist/components/tooltip/Tooltip.d.ts.map +1 -1
  40. package/dist/components/tooltip/Tooltip.js +10 -5
  41. package/dist/components/tooltip/Tooltip.js.map +1 -1
  42. package/dist/context/TranslationContext.d.ts.map +1 -1
  43. package/dist/context/TranslationContext.js +3 -3
  44. package/dist/context/TranslationContext.js.map +1 -1
  45. package/dist/index.d.ts +2 -2
  46. package/dist/index.d.ts.map +1 -1
  47. package/dist/index.js +1 -1
  48. package/dist/index.js.map +1 -1
  49. package/dist/internal/overlay/OverlayProvider.d.ts.map +1 -1
  50. package/dist/internal/overlay/OverlayProvider.js +17 -16
  51. package/dist/internal/overlay/OverlayProvider.js.map +1 -1
  52. package/dist/internal/overlay/Portal.d.ts.map +1 -1
  53. package/dist/internal/overlay/Portal.js +2 -1
  54. package/dist/internal/overlay/Portal.js.map +1 -1
  55. package/dist/internal/resolvePointerEvents.d.ts +9 -0
  56. package/dist/internal/resolvePointerEvents.d.ts.map +1 -0
  57. package/dist/internal/resolvePointerEvents.js +17 -0
  58. package/dist/internal/resolvePointerEvents.js.map +1 -0
  59. package/dist/layout/AppBar.js +1 -1
  60. package/dist/layout/AppBar.js.map +1 -1
  61. package/dist/layout/Box.d.ts.map +1 -1
  62. package/dist/layout/Box.js +4 -22
  63. package/dist/layout/Box.js.map +1 -1
  64. package/dist/layout/Grid.js.map +1 -1
  65. package/dist/layout/Stack.d.ts +1 -1
  66. package/dist/layout/Stack.d.ts.map +1 -1
  67. package/dist/layout/Stack.js +2 -13
  68. package/dist/layout/Stack.js.map +1 -1
  69. package/dist/primitives/icon/Icon.d.ts +5 -12
  70. package/dist/primitives/icon/Icon.d.ts.map +1 -1
  71. package/dist/primitives/icon/Icon.js +5 -11
  72. package/dist/primitives/icon/Icon.js.map +1 -1
  73. package/dist/primitives/icon/PortableIcon.d.ts +51 -0
  74. package/dist/primitives/icon/PortableIcon.d.ts.map +1 -0
  75. package/dist/primitives/icon/PortableIcon.js +64 -0
  76. package/dist/primitives/icon/PortableIcon.js.map +1 -0
  77. package/dist/primitives/icon/index.d.ts +2 -1
  78. package/dist/primitives/icon/index.d.ts.map +1 -1
  79. package/dist/primitives/icon/index.js +1 -0
  80. package/dist/primitives/icon/index.js.map +1 -1
  81. package/dist/primitives/image/Image.d.ts.map +1 -1
  82. package/dist/primitives/image/Image.js +3 -5
  83. package/dist/primitives/image/Image.js.map +1 -1
  84. package/package.json +30 -27
  85. package/src/components/action-sheet/ActionSheet.tsx +14 -8
  86. package/src/components/button/Button.tsx +5 -17
  87. package/src/components/button/types.ts +2 -5
  88. package/src/components/drawer/Drawer.tsx +14 -8
  89. package/src/components/icon-button/IconButton.tsx +1 -6
  90. package/src/components/icon-button/types.ts +2 -5
  91. package/src/components/menu/Menu.tsx +14 -8
  92. package/src/components/modal/Modal.tsx +14 -8
  93. package/src/components/navigation/NavigationItem.tsx +1 -6
  94. package/src/components/navigation/TabBarItem.tsx +1 -6
  95. package/src/components/navigation/types.ts +2 -2
  96. package/src/components/toast/ToastProvider.tsx +3 -2
  97. package/src/components/tooltip/Tooltip.tsx +12 -6
  98. package/src/context/TranslationContext.tsx +12 -15
  99. package/src/index.test.ts +1 -0
  100. package/src/index.ts +2 -2
  101. package/src/internal/overlay/OverlayProvider.tsx +20 -20
  102. package/src/internal/overlay/Portal.tsx +2 -1
  103. package/src/internal/resolvePointerEvents.test.ts +19 -0
  104. package/src/internal/resolvePointerEvents.ts +29 -0
  105. package/src/layout/AppBar.tsx +1 -1
  106. package/src/layout/Box.tsx +5 -36
  107. package/src/layout/Grid.tsx +3 -3
  108. package/src/layout/Stack.test.ts +14 -0
  109. package/src/layout/Stack.tsx +3 -17
  110. package/src/primitives/icon/Icon.tsx +9 -22
  111. package/src/primitives/icon/PortableIcon.tsx +112 -0
  112. package/src/primitives/icon/PortableIcon.web.test.tsx +119 -0
  113. package/src/primitives/icon/index.ts +2 -1
  114. package/src/primitives/image/Image.tsx +3 -6
  115. package/src/theme/colorEngine.test.ts +2 -2
  116. package/src/webAcceptance.test.tsx +84 -0
  117. package/dist/primitives/icon/resolveExpoIconComponent.d.ts +0 -11
  118. package/dist/primitives/icon/resolveExpoIconComponent.d.ts.map +0 -1
  119. package/dist/primitives/icon/resolveExpoIconComponent.js +0 -20
  120. package/dist/primitives/icon/resolveExpoIconComponent.js.map +0 -1
  121. package/src/primitives/icon/resolveExpoIconComponent.test.ts +0 -45
  122. package/src/primitives/icon/resolveExpoIconComponent.ts +0 -33
@@ -31,11 +31,9 @@ export function Button({
31
31
  {...props}
32
32
  disabled={disabled || loading}
33
33
  onPress={onPress}
34
- style={[
35
- {
36
- alignSelf: fullWidth ? 'stretch' : 'flex-start',
37
- },
38
- ]}
34
+ style={{
35
+ alignSelf: fullWidth ? 'stretch' : 'flex-start',
36
+ }}
39
37
  testID={testID}
40
38
  >
41
39
  {(state) => {
@@ -74,12 +72,7 @@ export function Button({
74
72
  <>
75
73
  {leadingIcon ? (
76
74
  <View style={{ marginRight: theme.spacing.s }}>
77
- <Icon
78
- color={colors.contentColor}
79
- name={leadingIcon.name}
80
- provider={leadingIcon.provider}
81
- size={iconSize}
82
- />
75
+ <Icon {...leadingIcon} color={colors.contentColor} size={iconSize} />
83
76
  </View>
84
77
  ) : null}
85
78
  <Text
@@ -92,12 +85,7 @@ export function Button({
92
85
  </Text>
93
86
  {trailingIcon ? (
94
87
  <View style={{ marginLeft: theme.spacing.s }}>
95
- <Icon
96
- color={colors.contentColor}
97
- name={trailingIcon.name}
98
- provider={trailingIcon.provider}
99
- size={iconSize}
100
- />
88
+ <Icon {...trailingIcon} color={colors.contentColor} size={iconSize} />
101
89
  </View>
102
90
  ) : null}
103
91
  </>
@@ -3,13 +3,10 @@ import type React from 'react';
3
3
  import type { ControlSize } from '../../internal/resolvers/resolveControlSize';
4
4
  import type { ButtonVariant } from '../../internal/resolvers/resolveInteractiveColors';
5
5
  import type { ButtonBaseProps } from '../../primitives/button-base';
6
- import type { IconProps } from '../../primitives/icon';
6
+ import type { IconSource } from '../../primitives/icon';
7
7
  import type { SurfaceColor } from '../../surfaceColor';
8
8
 
9
- export interface ButtonIconSpec {
10
- name: IconProps['name'];
11
- provider?: IconProps['provider'];
12
- }
9
+ export type ButtonIconSpec = IconSource;
13
10
 
14
11
  export interface ButtonProps extends Omit<
15
12
  ButtonBaseProps,
@@ -3,11 +3,14 @@ import { Pressable, View } from 'react-native';
3
3
 
4
4
  import { FocusScope } from '../../internal/focus/FocusScope';
5
5
  import { Portal } from '../../internal/overlay/Portal';
6
+ import { resolvePointerEvents } from '../../internal/resolvePointerEvents';
6
7
  import { resolveOverlayAnimation } from '../../internal/resolvers';
7
8
  import { Box, Surface } from '../../layout';
8
9
  import { useTheme } from '../../theme/ThemeContext';
9
10
  import type { DrawerProps } from './types';
10
11
 
12
+ const boxNonePointerEvents = resolvePointerEvents('box-none');
13
+
11
14
  export function Drawer({
12
15
  visible,
13
16
  onDismiss,
@@ -28,14 +31,17 @@ export function Drawer({
28
31
  return (
29
32
  <Portal layer="drawer" visible={visible}>
30
33
  <View
31
- pointerEvents="box-none"
32
- style={{
33
- bottom: 0,
34
- left: 0,
35
- position: 'absolute',
36
- right: 0,
37
- top: 0,
38
- }}
34
+ {...boxNonePointerEvents.props}
35
+ style={[
36
+ boxNonePointerEvents.style,
37
+ {
38
+ bottom: 0,
39
+ left: 0,
40
+ position: 'absolute',
41
+ right: 0,
42
+ top: 0,
43
+ },
44
+ ]}
39
45
  >
40
46
  <Pressable
41
47
  onPress={passive ? undefined : closeOnBackdrop ? onDismiss : undefined}
@@ -46,12 +46,7 @@ export function IconButton({
46
46
  opacity: colors.opacity,
47
47
  }}
48
48
  >
49
- <Icon
50
- color={colors.contentColor}
51
- name={icon.name}
52
- provider={icon.provider}
53
- size={resolveIconSize(theme, size)}
54
- />
49
+ <Icon {...icon} color={colors.contentColor} size={resolveIconSize(theme, size)} />
55
50
  </Box>
56
51
  );
57
52
  }}
@@ -1,17 +1,14 @@
1
1
  import type { ControlSize } from '../../internal/resolvers/resolveControlSize';
2
2
  import type { ButtonVariant } from '../../internal/resolvers/resolveInteractiveColors';
3
3
  import type { ButtonBaseProps } from '../../primitives/button-base';
4
- import type { IconProps } from '../../primitives/icon';
4
+ import type { IconSource } from '../../primitives/icon';
5
5
  import type { SurfaceColor } from '../../surfaceColor';
6
6
 
7
7
  export interface IconButtonProps extends Omit<
8
8
  ButtonBaseProps,
9
9
  'children' | 'style' | 'accessibilityLabel'
10
10
  > {
11
- icon: {
12
- name: IconProps['name'];
13
- provider?: IconProps['provider'];
14
- };
11
+ icon: IconSource;
15
12
  accessibilityLabel: string;
16
13
  variant?: ButtonVariant;
17
14
  color?: SurfaceColor;
@@ -4,6 +4,7 @@ import { type LayoutRectangle, Pressable, View } from 'react-native';
4
4
  import { FocusScope } from '../../internal/focus/FocusScope';
5
5
  import { useFocusManager } from '../../internal/focus/useFocusManager';
6
6
  import { Portal } from '../../internal/overlay/Portal';
7
+ import { resolvePointerEvents } from '../../internal/resolvePointerEvents';
7
8
  import { resolveOverlayAnimation } from '../../internal/resolvers';
8
9
  import { Box, Inline, Stack, Surface } from '../../layout';
9
10
  import { ButtonBase } from '../../primitives/button-base';
@@ -12,6 +13,8 @@ import { useTheme } from '../../theme/ThemeContext';
12
13
  import { resolveNextMenuIndex } from './navigation';
13
14
  import type { MenuAction, MenuProps } from './types';
14
15
 
16
+ const boxNonePointerEvents = resolvePointerEvents('box-none');
17
+
15
18
  interface MeasurableNode {
16
19
  measureInWindow?: (
17
20
  callback: (x: number, y: number, width: number, height: number) => void,
@@ -142,14 +145,17 @@ export function Menu({
142
145
  </ButtonBase>
143
146
  <Portal layer="menu" visible={open && Boolean(layout)}>
144
147
  <View
145
- pointerEvents="box-none"
146
- style={{
147
- bottom: 0,
148
- left: 0,
149
- position: 'absolute',
150
- right: 0,
151
- top: 0,
152
- }}
148
+ {...boxNonePointerEvents.props}
149
+ style={[
150
+ boxNonePointerEvents.style,
151
+ {
152
+ bottom: 0,
153
+ left: 0,
154
+ position: 'absolute',
155
+ right: 0,
156
+ top: 0,
157
+ },
158
+ ]}
153
159
  >
154
160
  <Pressable
155
161
  onPress={passive ? undefined : closeMenu}
@@ -3,11 +3,14 @@ import { Pressable, View } from 'react-native';
3
3
 
4
4
  import { FocusScope } from '../../internal/focus/FocusScope';
5
5
  import { Portal } from '../../internal/overlay/Portal';
6
+ import { resolvePointerEvents } from '../../internal/resolvePointerEvents';
6
7
  import { resolveOverlayAnimation } from '../../internal/resolvers';
7
8
  import { Center, Surface } from '../../layout';
8
9
  import { useTheme } from '../../theme/ThemeContext';
9
10
  import type { ModalProps } from './types';
10
11
 
12
+ const boxNonePointerEvents = resolvePointerEvents('box-none');
13
+
11
14
  export function Modal({
12
15
  visible,
13
16
  onDismiss,
@@ -27,14 +30,17 @@ export function Modal({
27
30
  return (
28
31
  <Portal layer="modal" visible={visible}>
29
32
  <View
30
- pointerEvents="box-none"
31
- style={{
32
- bottom: 0,
33
- left: 0,
34
- position: 'absolute',
35
- right: 0,
36
- top: 0,
37
- }}
33
+ {...boxNonePointerEvents.props}
34
+ style={[
35
+ boxNonePointerEvents.style,
36
+ {
37
+ bottom: 0,
38
+ left: 0,
39
+ position: 'absolute',
40
+ right: 0,
41
+ top: 0,
42
+ },
43
+ ]}
38
44
  >
39
45
  <Pressable
40
46
  onPress={passive ? undefined : closeOnBackdrop ? onDismiss : undefined}
@@ -56,12 +56,7 @@ export function NavigationItem({
56
56
  >
57
57
  {item.icon ? (
58
58
  <View style={{ marginRight: theme.spacing.s }}>
59
- <Icon
60
- color={presentation.contentColor}
61
- name={item.icon.name}
62
- provider={item.icon.provider}
63
- size={compact ? 's' : 'm'}
64
- />
59
+ <Icon {...item.icon} color={presentation.contentColor} size={compact ? 's' : 'm'} />
65
60
  </View>
66
61
  ) : null}
67
62
  <Box flex={1}>
@@ -57,12 +57,7 @@ export function TabBarItem({ item, compact = false, testID, interactionPolicy }:
57
57
  >
58
58
  {item.icon ? (
59
59
  <View style={{ marginBottom: item.label ? theme.spacing.xs : 0 }}>
60
- <Icon
61
- color={contentColor}
62
- name={item.icon.name}
63
- provider={item.icon.provider}
64
- size={compact ? 's' : 'm'}
65
- />
60
+ <Icon {...item.icon} color={contentColor} size={compact ? 's' : 'm'} />
66
61
  </View>
67
62
  ) : null}
68
63
  {item.label ? (
@@ -2,9 +2,9 @@ import type React from 'react';
2
2
  import type { AccessibilityRole, AccessibilityState } from 'react-native';
3
3
 
4
4
  import type { InteractionPolicy } from '../../interactionPolicy';
5
- import type { IconProps } from '../../primitives/icon';
5
+ import type { IconSource } from '../../primitives/icon';
6
6
 
7
- export type NavigationItemIcon = Pick<IconProps, 'name' | 'provider'>;
7
+ export type NavigationItemIcon = IconSource;
8
8
 
9
9
  export interface NavigationItemSpec {
10
10
  id: string;
@@ -68,9 +68,10 @@ export function ToastProvider({
68
68
  }, [defaultDuration, dismissToast, toasts]);
69
69
 
70
70
  React.useEffect(() => {
71
+ const timers = timersRef.current;
71
72
  return () => {
72
- timersRef.current.forEach((timer) => clearTimeout(timer));
73
- timersRef.current.clear();
73
+ timers.forEach((timer) => clearTimeout(timer));
74
+ timers.clear();
74
75
  };
75
76
  }, []);
76
77
 
@@ -2,12 +2,15 @@ import React from 'react';
2
2
  import { type LayoutRectangle, Platform, Pressable, View } from 'react-native';
3
3
 
4
4
  import { Portal } from '../../internal/overlay/Portal';
5
+ import { resolvePointerEvents } from '../../internal/resolvePointerEvents';
5
6
  import { resolveOverlayAnimation } from '../../internal/resolvers';
6
7
  import { Surface } from '../../layout';
7
8
  import { Text } from '../../primitives/text';
8
9
  import { useTheme } from '../../theme/ThemeContext';
9
10
  import type { TooltipProps } from './types';
10
11
 
12
+ const noPointerEvents = resolvePointerEvents('none');
13
+
11
14
  interface MeasurableNode {
12
15
  measureInWindow?: (
13
16
  callback: (x: number, y: number, width: number, height: number) => void,
@@ -85,12 +88,15 @@ export function Tooltip({
85
88
  </Pressable>
86
89
  <Portal layer="tooltip" visible={visible && Boolean(layout)}>
87
90
  <View
88
- pointerEvents="none"
89
- style={{
90
- left: tooltipLeft,
91
- position: 'absolute',
92
- top: tooltipTop,
93
- }}
91
+ {...noPointerEvents.props}
92
+ style={[
93
+ noPointerEvents.style,
94
+ {
95
+ left: tooltipLeft,
96
+ position: 'absolute',
97
+ top: tooltipTop,
98
+ },
99
+ ]}
94
100
  >
95
101
  <Surface
96
102
  p="s"
@@ -1,4 +1,4 @@
1
- import React, { createContext, useContext, useMemo } from 'react';
1
+ import React, { createContext, useContext } from 'react';
2
2
 
3
3
  /**
4
4
  * Minimal translation runtime surface.
@@ -31,20 +31,17 @@ export function TranslationProvider(props: {
31
31
  }) {
32
32
  const { t, i18n, children } = props;
33
33
 
34
- const value = useMemo<TranslationRuntime>(
35
- () => ({
36
- t: (key: string, options?: Record<string, unknown>) => {
37
- if (i18n?.t) {
38
- const result = i18n.t(key, options);
39
- // If translation returns the key, it's missing in the current dictionary
40
- if (result !== key) return result;
41
- }
42
- return t(key, options);
43
- },
44
- i18n: i18n ?? null,
45
- }),
46
- [t, i18n, i18n?.language],
47
- );
34
+ const value: TranslationRuntime = {
35
+ t: (key: string, options?: Record<string, unknown>) => {
36
+ if (i18n?.t) {
37
+ const result = i18n.t(key, options);
38
+ // If translation returns the key, it's missing in the current dictionary
39
+ if (result !== key) return result;
40
+ }
41
+ return t(key, options);
42
+ },
43
+ i18n: i18n ?? null,
44
+ };
48
45
 
49
46
  return <TranslationContext.Provider value={value}>{children}</TranslationContext.Provider>;
50
47
  }
package/src/index.test.ts CHANGED
@@ -19,6 +19,7 @@ const expectedRootExports = [
19
19
  "export { Field } from './components/field';",
20
20
  "export { HelperText } from './components/helper-text';",
21
21
  "export { IconButton } from './components/icon-button';",
22
+ "export { Icon, SUPPORTED_ICON_PROVIDERS } from './primitives/icon';",
22
23
  "export type { InteractionPolicy, InteractionPolicyProps } from './interactionPolicy';",
23
24
  "export { Label } from './components/label';",
24
25
  "export { ListItem } from './components/list-item';",
package/src/index.ts CHANGED
@@ -67,8 +67,8 @@ export type { ButtonBaseProps } from './primitives/button-base';
67
67
  export { ButtonBase } from './primitives/button-base';
68
68
  export type { HeadingLevel, HeadingProps } from './primitives/heading';
69
69
  export { Heading } from './primitives/heading';
70
- export type { IconProps, IconProvider } from './primitives/icon';
71
- export { Icon } from './primitives/icon';
70
+ export type { IconProps, IconProvider, IconSource, IconVariant } from './primitives/icon';
71
+ export { Icon, SUPPORTED_ICON_PROVIDERS } from './primitives/icon';
72
72
  export type { ImageFit, ImageProps, SurfaceImageSource } from './primitives/image';
73
73
  export { Image } from './primitives/image';
74
74
  export type { TextProps } from './primitives/text';
@@ -1,6 +1,7 @@
1
1
  import React from 'react';
2
- import { View } from 'react-native';
2
+ import { StyleSheet, View, type ViewStyle } from 'react-native';
3
3
 
4
+ import { resolvePointerEvents } from '../resolvePointerEvents';
4
5
  import {
5
6
  createOverlayEntry,
6
7
  type OverlayDescriptor,
@@ -10,6 +11,21 @@ import {
10
11
  sortOverlayEntries,
11
12
  } from './useOverlayStack';
12
13
 
14
+ const boxNonePointerEvents = resolvePointerEvents('box-none');
15
+ const styles = StyleSheet.create({
16
+ fill: {
17
+ bottom: 0,
18
+ left: 0,
19
+ position: 'absolute',
20
+ right: 0,
21
+ top: 0,
22
+ },
23
+ });
24
+
25
+ function overlayZIndex(zIndex: number): ViewStyle {
26
+ return { zIndex };
27
+ }
28
+
13
29
  export function OverlayProvider({ children }: { children: React.ReactNode }) {
14
30
  const orderRef = React.useRef(0);
15
31
  const [overlays, setOverlays] = React.useState<OverlayEntry[]>([]);
@@ -52,28 +68,12 @@ export function OverlayProvider({ children }: { children: React.ReactNode }) {
52
68
  <OverlayStackActionsContext.Provider value={actions}>
53
69
  <OverlayStackContext.Provider value={value}>
54
70
  {children}
55
- <View
56
- pointerEvents="box-none"
57
- style={{
58
- bottom: 0,
59
- left: 0,
60
- position: 'absolute',
61
- right: 0,
62
- top: 0,
63
- }}
64
- >
71
+ <View {...boxNonePointerEvents.props} style={[boxNonePointerEvents.style, styles.fill]}>
65
72
  {overlays.map((overlay) => (
66
73
  <View
74
+ {...boxNonePointerEvents.props}
67
75
  key={overlay.id}
68
- pointerEvents="box-none"
69
- style={{
70
- bottom: 0,
71
- left: 0,
72
- position: 'absolute',
73
- right: 0,
74
- top: 0,
75
- zIndex: overlay.zIndex,
76
- }}
76
+ style={[boxNonePointerEvents.style, styles.fill, overlayZIndex(overlay.zIndex)]}
77
77
  >
78
78
  {overlay.node}
79
79
  </View>
@@ -19,9 +19,10 @@ export function Portal({ children, layer = 'modal', visible = true }: PortalProp
19
19
  if (!overlayActions || !visible) {
20
20
  return undefined;
21
21
  }
22
+ const id = idRef.current;
22
23
 
23
24
  return () => {
24
- overlayActions.removeOverlay(idRef.current);
25
+ overlayActions.removeOverlay(id);
25
26
  };
26
27
  }, [overlayActions, visible]);
27
28
 
@@ -0,0 +1,19 @@
1
+ import { describe, expect, test } from 'bun:test';
2
+
3
+ import { resolvePointerEventsForPlatform } from './resolvePointerEvents';
4
+
5
+ describe('resolvePointerEvents', () => {
6
+ test('moves React Native Web pointer events into style', () => {
7
+ expect(resolvePointerEventsForPlatform('box-none', 'web')).toEqual({
8
+ props: {},
9
+ style: { pointerEvents: 'box-none' },
10
+ });
11
+ });
12
+
13
+ test('preserves the native pointer-events prop', () => {
14
+ expect(resolvePointerEventsForPlatform('none', 'ios')).toEqual({
15
+ props: { pointerEvents: 'none' },
16
+ style: null,
17
+ });
18
+ });
19
+ });
@@ -0,0 +1,29 @@
1
+ import { Platform, type StyleProp, type ViewProps, type ViewStyle } from 'react-native';
2
+
3
+ interface ResolvedPointerEvents {
4
+ props: Pick<ViewProps, 'pointerEvents'>;
5
+ style: StyleProp<ViewStyle>;
6
+ }
7
+
8
+ export function resolvePointerEvents(
9
+ pointerEvents: NonNullable<ViewProps['pointerEvents']>,
10
+ ): ResolvedPointerEvents {
11
+ return resolvePointerEventsForPlatform(pointerEvents, Platform.OS);
12
+ }
13
+
14
+ export function resolvePointerEventsForPlatform(
15
+ pointerEvents: NonNullable<ViewProps['pointerEvents']>,
16
+ platform: typeof Platform.OS,
17
+ ): ResolvedPointerEvents {
18
+ if (platform === 'web') {
19
+ return {
20
+ props: {},
21
+ style: { pointerEvents },
22
+ };
23
+ }
24
+
25
+ return {
26
+ props: { pointerEvents },
27
+ style: null,
28
+ };
29
+ }
@@ -33,7 +33,7 @@ export function AppBar({
33
33
  topInset === 0 ? undefined : { paddingTop: topInset };
34
34
 
35
35
  return (
36
- <Box {...props} bg={bg ?? theme.semantics.surface.default} style={[style]}>
36
+ <Box {...props} bg={bg ?? theme.semantics.surface.default} style={style}>
37
37
  <View style={safeAreaStyle}>
38
38
  <View
39
39
  style={[
@@ -1,13 +1,8 @@
1
1
  import React from 'react';
2
- import {
3
- Platform,
4
- type StyleProp,
5
- View,
6
- type ViewProps as ReactNativeViewProps,
7
- type ViewStyle,
8
- } from 'react-native';
2
+ import { View, type ViewProps as ReactNativeViewProps } from 'react-native';
9
3
 
10
4
  import { useResponsiveRuntime } from '../core/responsive';
5
+ import { resolvePointerEvents } from '../internal/resolvePointerEvents';
11
6
  import { useTheme } from '../theme/ThemeContext';
12
7
  import { type BoxStyleProps, resolveBoxStyles } from './helpers';
13
8
 
@@ -21,31 +16,6 @@ export interface BoxProps extends BoxStyleProps {
21
16
  testID?: string;
22
17
  }
23
18
 
24
- function resolvePointerEventsStyle(
25
- pointerEvents: ReactNativeViewProps['pointerEvents'] | undefined,
26
- ): StyleProp<ViewStyle> | null {
27
- if (Platform.OS !== 'web' || pointerEvents === undefined) return null;
28
-
29
- if (pointerEvents === 'auto' || pointerEvents === 'none') {
30
- return { pointerEvents };
31
- }
32
-
33
- return null;
34
- }
35
-
36
- function resolveViewPointerEvents(
37
- pointerEvents: ReactNativeViewProps['pointerEvents'] | undefined,
38
- ): ReactNativeViewProps['pointerEvents'] | undefined {
39
- if (pointerEvents === undefined) return undefined;
40
- if (Platform.OS !== 'web') return pointerEvents;
41
-
42
- if (pointerEvents === 'box-none' || pointerEvents === 'box-only') {
43
- return pointerEvents;
44
- }
45
-
46
- return undefined;
47
- }
48
-
49
19
  export function Box({
50
20
  accessible,
51
21
  accessibilityLabel,
@@ -60,18 +30,17 @@ export function Box({
60
30
  const { theme } = useTheme();
61
31
  const { breakpoint } = useResponsiveRuntime();
62
32
  const resolved = resolveBoxStyles(theme, breakpoint, props);
63
- const pointerEventsStyle = resolvePointerEventsStyle(pointerEvents);
64
- const viewPointerEvents = resolveViewPointerEvents(pointerEvents);
33
+ const resolvedPointerEvents = pointerEvents ? resolvePointerEvents(pointerEvents) : null;
65
34
 
66
35
  return (
67
36
  <View
37
+ {...resolvedPointerEvents?.props}
68
38
  accessible={accessible}
69
39
  accessibilityLabel={accessibilityLabel}
70
40
  accessibilityRole={accessibilityRole}
71
41
  accessibilityState={accessibilityState}
72
- pointerEvents={viewPointerEvents}
73
42
  testID={testID}
74
- style={[resolved, pointerEventsStyle, style]}
43
+ style={[resolved, resolvedPointerEvents?.style, style]}
75
44
  >
76
45
  {children}
77
46
  </View>
@@ -35,7 +35,7 @@ export function Grid({
35
35
  const colSpacing = Number(resolveSpacing(theme, activeColGap) ?? 0);
36
36
  const activeMinItemWidth = resolveResponsive(minItemWidth, breakpoint);
37
37
 
38
- const basisPercent = `${100 / activeCols}%`;
38
+ const basisPercent: `${number}%` = `${100 / activeCols}%`;
39
39
  const nodes = React.Children.toArray(children);
40
40
 
41
41
  return (
@@ -58,8 +58,8 @@ export function Grid({
58
58
  flexGrow: 1,
59
59
  }
60
60
  : {
61
- width: basisPercent as ViewStyle['width'],
62
- flexBasis: basisPercent as ViewStyle['flexBasis'],
61
+ width: basisPercent,
62
+ flexBasis: basisPercent,
63
63
  };
64
64
 
65
65
  return (
@@ -0,0 +1,14 @@
1
+ import { readFile } from 'node:fs/promises';
2
+
3
+ import { describe, expect, it } from 'bun:test';
4
+
5
+ describe('Stack layout contract', () => {
6
+ it('keeps authored children as direct flex items and applies spacing through gap', async () => {
7
+ const source = await readFile(new URL('./Stack.tsx', import.meta.url), 'utf8');
8
+
9
+ expect(source).toContain('gap: activeGap');
10
+ expect(source).toContain('{children}');
11
+ expect(source).not.toContain("from 'react-native'");
12
+ expect(source).not.toContain('React.Children');
13
+ });
14
+ });
@@ -1,5 +1,4 @@
1
- import React from 'react';
2
- import { View } from 'react-native';
1
+ import type React from 'react';
3
2
 
4
3
  import { resolveResponsive, type Responsive, useResponsiveRuntime } from '../core/responsive';
5
4
  import { useTheme } from '../theme/ThemeContext';
@@ -34,14 +33,13 @@ export function Stack({
34
33
  const activeJustify = resolveResponsive(justify, breakpoint);
35
34
  const activeWrap = resolveResponsive(wrap, breakpoint);
36
35
 
37
- const nodes = React.Children.toArray(children);
38
-
39
36
  return (
40
37
  <Box
41
38
  {...props}
42
39
  style={[
43
40
  {
44
41
  flexDirection: activeDirection,
42
+ gap: activeGap,
45
43
  alignItems: activeAlign,
46
44
  justifyContent: activeJustify,
47
45
  flexWrap: activeWrap,
@@ -49,19 +47,7 @@ export function Stack({
49
47
  props.style,
50
48
  ]}
51
49
  >
52
- {nodes.map((node, index) => {
53
- const spacing =
54
- index === 0
55
- ? undefined
56
- : activeDirection === 'row'
57
- ? { marginLeft: activeGap }
58
- : { marginTop: activeGap };
59
- return (
60
- <View key={String(index)} style={spacing}>
61
- {node}
62
- </View>
63
- );
64
- })}
50
+ {children}
65
51
  </Box>
66
52
  );
67
53
  }