@ankhorage/surface 2.0.3 → 2.1.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 (134) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/dist/components/action-sheet/ActionSheet.d.ts +1 -1
  3. package/dist/components/action-sheet/ActionSheet.d.ts.map +1 -1
  4. package/dist/components/action-sheet/ActionSheet.js +5 -4
  5. package/dist/components/action-sheet/ActionSheet.js.map +1 -1
  6. package/dist/components/action-sheet/ActionSheetItem.d.ts +1 -1
  7. package/dist/components/action-sheet/ActionSheetItem.d.ts.map +1 -1
  8. package/dist/components/action-sheet/ActionSheetItem.js +2 -2
  9. package/dist/components/action-sheet/ActionSheetItem.js.map +1 -1
  10. package/dist/components/action-sheet/types.d.ts +3 -0
  11. package/dist/components/action-sheet/types.d.ts.map +1 -1
  12. package/dist/components/action-sheet/types.js.map +1 -1
  13. package/dist/components/drawer/Drawer.d.ts +1 -1
  14. package/dist/components/drawer/Drawer.d.ts.map +1 -1
  15. package/dist/components/drawer/Drawer.js +4 -3
  16. package/dist/components/drawer/Drawer.js.map +1 -1
  17. package/dist/components/drawer/types.d.ts +2 -0
  18. package/dist/components/drawer/types.d.ts.map +1 -1
  19. package/dist/components/drawer/types.js.map +1 -1
  20. package/dist/components/menu/Menu.d.ts +1 -1
  21. package/dist/components/menu/Menu.d.ts.map +1 -1
  22. package/dist/components/menu/Menu.js +10 -6
  23. package/dist/components/menu/Menu.js.map +1 -1
  24. package/dist/components/menu/types.d.ts +2 -0
  25. package/dist/components/menu/types.d.ts.map +1 -1
  26. package/dist/components/menu/types.js.map +1 -1
  27. package/dist/components/modal/Modal.d.ts +1 -1
  28. package/dist/components/modal/Modal.d.ts.map +1 -1
  29. package/dist/components/modal/Modal.js +4 -3
  30. package/dist/components/modal/Modal.js.map +1 -1
  31. package/dist/components/modal/types.d.ts +2 -0
  32. package/dist/components/modal/types.d.ts.map +1 -1
  33. package/dist/components/modal/types.js.map +1 -1
  34. package/dist/components/navigation/DrawerNavigation.d.ts +1 -1
  35. package/dist/components/navigation/DrawerNavigation.d.ts.map +1 -1
  36. package/dist/components/navigation/DrawerNavigation.js +2 -2
  37. package/dist/components/navigation/DrawerNavigation.js.map +1 -1
  38. package/dist/components/navigation/DrawerNavigationItem.d.ts +1 -1
  39. package/dist/components/navigation/DrawerNavigationItem.d.ts.map +1 -1
  40. package/dist/components/navigation/DrawerNavigationItem.js +2 -2
  41. package/dist/components/navigation/DrawerNavigationItem.js.map +1 -1
  42. package/dist/components/navigation/NavigationItem.d.ts +1 -1
  43. package/dist/components/navigation/NavigationItem.d.ts.map +1 -1
  44. package/dist/components/navigation/NavigationItem.js +2 -2
  45. package/dist/components/navigation/NavigationItem.js.map +1 -1
  46. package/dist/components/navigation/NavigationList.d.ts +1 -1
  47. package/dist/components/navigation/NavigationList.d.ts.map +1 -1
  48. package/dist/components/navigation/NavigationList.js +2 -2
  49. package/dist/components/navigation/NavigationList.js.map +1 -1
  50. package/dist/components/navigation/TabBar.d.ts +1 -1
  51. package/dist/components/navigation/TabBar.d.ts.map +1 -1
  52. package/dist/components/navigation/TabBar.js +2 -2
  53. package/dist/components/navigation/TabBar.js.map +1 -1
  54. package/dist/components/navigation/TabBarItem.d.ts +1 -1
  55. package/dist/components/navigation/TabBarItem.d.ts.map +1 -1
  56. package/dist/components/navigation/TabBarItem.js +2 -2
  57. package/dist/components/navigation/TabBarItem.js.map +1 -1
  58. package/dist/components/navigation/types.d.ts +7 -0
  59. package/dist/components/navigation/types.d.ts.map +1 -1
  60. package/dist/components/navigation/types.js.map +1 -1
  61. package/dist/components/tabs/Tab.d.ts +1 -1
  62. package/dist/components/tabs/Tab.d.ts.map +1 -1
  63. package/dist/components/tabs/Tab.js +9 -6
  64. package/dist/components/tabs/Tab.js.map +1 -1
  65. package/dist/components/tabs/types.d.ts +2 -0
  66. package/dist/components/tabs/types.d.ts.map +1 -1
  67. package/dist/components/tabs/types.js.map +1 -1
  68. package/dist/components/text-input/TextInput.d.ts +1 -1
  69. package/dist/components/text-input/TextInput.d.ts.map +1 -1
  70. package/dist/components/text-input/TextInput.js +8 -2
  71. package/dist/components/text-input/TextInput.js.map +1 -1
  72. package/dist/components/text-input/types.d.ts +2 -0
  73. package/dist/components/text-input/types.d.ts.map +1 -1
  74. package/dist/components/text-input/types.js.map +1 -1
  75. package/dist/components/toast/Toast.d.ts +1 -1
  76. package/dist/components/toast/Toast.d.ts.map +1 -1
  77. package/dist/components/toast/Toast.js +3 -2
  78. package/dist/components/toast/Toast.js.map +1 -1
  79. package/dist/components/toast/types.d.ts +2 -0
  80. package/dist/components/toast/types.d.ts.map +1 -1
  81. package/dist/components/toast/types.js.map +1 -1
  82. package/dist/components/tooltip/Tooltip.d.ts +1 -1
  83. package/dist/components/tooltip/Tooltip.d.ts.map +1 -1
  84. package/dist/components/tooltip/Tooltip.js +3 -2
  85. package/dist/components/tooltip/Tooltip.js.map +1 -1
  86. package/dist/components/tooltip/types.d.ts +2 -0
  87. package/dist/components/tooltip/types.d.ts.map +1 -1
  88. package/dist/components/tooltip/types.js.map +1 -1
  89. package/dist/index.d.ts +1 -0
  90. package/dist/index.d.ts.map +1 -1
  91. package/dist/index.js.map +1 -1
  92. package/dist/interactionPolicy.d.ts +5 -0
  93. package/dist/interactionPolicy.d.ts.map +1 -0
  94. package/dist/interactionPolicy.js +2 -0
  95. package/dist/interactionPolicy.js.map +1 -0
  96. package/dist/primitives/button-base/ButtonBase.d.ts +1 -1
  97. package/dist/primitives/button-base/ButtonBase.d.ts.map +1 -1
  98. package/dist/primitives/button-base/ButtonBase.js +3 -2
  99. package/dist/primitives/button-base/ButtonBase.js.map +1 -1
  100. package/dist/primitives/button-base/types.d.ts +2 -1
  101. package/dist/primitives/button-base/types.d.ts.map +1 -1
  102. package/dist/primitives/button-base/types.js.map +1 -1
  103. package/package.json +1 -1
  104. package/src/components/action-sheet/ActionSheet.tsx +5 -2
  105. package/src/components/action-sheet/ActionSheetItem.tsx +2 -0
  106. package/src/components/action-sheet/types.ts +3 -0
  107. package/src/components/drawer/Drawer.tsx +4 -2
  108. package/src/components/drawer/types.ts +3 -0
  109. package/src/components/menu/Menu.tsx +18 -6
  110. package/src/components/menu/types.ts +3 -0
  111. package/src/components/modal/Modal.tsx +4 -2
  112. package/src/components/modal/types.ts +3 -0
  113. package/src/components/navigation/DrawerNavigation.tsx +8 -1
  114. package/src/components/navigation/DrawerNavigationItem.tsx +7 -1
  115. package/src/components/navigation/NavigationItem.tsx +7 -1
  116. package/src/components/navigation/NavigationList.tsx +8 -1
  117. package/src/components/navigation/TabBar.tsx +8 -2
  118. package/src/components/navigation/TabBarItem.tsx +2 -1
  119. package/src/components/navigation/policy.test.ts +96 -0
  120. package/src/components/navigation/types.ts +7 -0
  121. package/src/components/tabs/Tab.tsx +17 -6
  122. package/src/components/tabs/types.ts +3 -0
  123. package/src/components/text-input/TextInput.tsx +10 -2
  124. package/src/components/text-input/types.ts +2 -0
  125. package/src/components/toast/Toast.tsx +10 -2
  126. package/src/components/toast/types.ts +2 -0
  127. package/src/components/tooltip/Tooltip.tsx +6 -4
  128. package/src/components/tooltip/types.ts +3 -0
  129. package/src/index.test.ts +1 -0
  130. package/src/index.ts +1 -0
  131. package/src/interactionPolicy.test.ts +86 -0
  132. package/src/interactionPolicy.ts +5 -0
  133. package/src/primitives/button-base/ButtonBase.tsx +4 -2
  134. package/src/primitives/button-base/types.ts +3 -1
@@ -10,13 +10,20 @@ export function DrawerNavigation({
10
10
  footer,
11
11
  compact = false,
12
12
  testID,
13
+ interactionPolicy,
13
14
  }: DrawerNavigationProps) {
14
15
  return (
15
16
  <Stack gap={compact ? 's' : 'm'} testID={testID}>
16
17
  {header}
17
18
  <Stack gap={compact ? 'xs' : 's'}>
18
19
  {items.map((item) => (
19
- <DrawerNavigationItem compact={compact} item={item} key={item.id} testID={testID} />
20
+ <DrawerNavigationItem
21
+ compact={compact}
22
+ interactionPolicy={interactionPolicy}
23
+ item={item}
24
+ key={item.id}
25
+ testID={testID}
26
+ />
20
27
  ))}
21
28
  </Stack>
22
29
  {footer}
@@ -3,10 +3,16 @@ import React from 'react';
3
3
  import { NavigationItem } from './NavigationItem';
4
4
  import type { DrawerNavigationItemProps } from './types';
5
5
 
6
- export function DrawerNavigationItem({ item, compact = false, testID }: DrawerNavigationItemProps) {
6
+ export function DrawerNavigationItem({
7
+ item,
8
+ compact = false,
9
+ testID,
10
+ interactionPolicy,
11
+ }: DrawerNavigationItemProps) {
7
12
  return (
8
13
  <NavigationItem
9
14
  compact={compact}
15
+ interactionPolicy={interactionPolicy}
10
16
  item={{
11
17
  ...item,
12
18
  accessibilityRole: item.accessibilityRole ?? 'button',
@@ -9,7 +9,12 @@ import { useTheme } from '../../theme/ThemeContext';
9
9
  import { resolveNavigationAccessibilityState, resolveNavigationItemPresentation } from './helpers';
10
10
  import type { NavigationItemProps } from './types';
11
11
 
12
- export function NavigationItem({ item, compact = false, testID }: NavigationItemProps) {
12
+ export function NavigationItem({
13
+ item,
14
+ compact = false,
15
+ testID,
16
+ interactionPolicy,
17
+ }: NavigationItemProps) {
13
18
  const { theme } = useTheme();
14
19
  const active = Boolean(item.active);
15
20
  const disabled = Boolean(item.disabled);
@@ -26,6 +31,7 @@ export function NavigationItem({ item, compact = false, testID }: NavigationItem
26
31
  accessibilityRole={item.accessibilityRole ?? 'button'}
27
32
  accessibilityState={resolvedAccessibilityState}
28
33
  disabled={disabled}
34
+ interactionPolicy={interactionPolicy}
29
35
  onPress={item.onPress}
30
36
  testID={resolvedTestID}
31
37
  >
@@ -9,6 +9,7 @@ export function NavigationList({
9
9
  orientation = 'vertical',
10
10
  compact = false,
11
11
  testID,
12
+ interactionPolicy,
12
13
  }: NavigationListProps) {
13
14
  return (
14
15
  <Stack
@@ -17,7 +18,13 @@ export function NavigationList({
17
18
  testID={testID}
18
19
  >
19
20
  {items.map((item) => (
20
- <NavigationItem compact={compact} item={item} key={item.id} testID={testID} />
21
+ <NavigationItem
22
+ compact={compact}
23
+ interactionPolicy={interactionPolicy}
24
+ item={item}
25
+ key={item.id}
26
+ testID={testID}
27
+ />
21
28
  ))}
22
29
  </Stack>
23
30
  );
@@ -5,7 +5,7 @@ import { useTheme } from '../../theme/ThemeContext';
5
5
  import { TabBarItem } from './TabBarItem';
6
6
  import type { TabBarProps } from './types';
7
7
 
8
- export function TabBar({ items, compact = false, testID }: TabBarProps) {
8
+ export function TabBar({ items, compact = false, testID, interactionPolicy }: TabBarProps) {
9
9
  const { theme } = useTheme();
10
10
 
11
11
  return (
@@ -18,7 +18,13 @@ export function TabBar({ items, compact = false, testID }: TabBarProps) {
18
18
  testID={testID}
19
19
  >
20
20
  {items.map((item) => (
21
- <TabBarItem compact={compact} item={item} key={item.id} testID={testID} />
21
+ <TabBarItem
22
+ compact={compact}
23
+ interactionPolicy={interactionPolicy}
24
+ item={item}
25
+ key={item.id}
26
+ testID={testID}
27
+ />
22
28
  ))}
23
29
  </Box>
24
30
  );
@@ -9,7 +9,7 @@ import { useTheme } from '../../theme/ThemeContext';
9
9
  import { resolveNavigationAccessibilityState, resolveNavigationItemPresentation } from './helpers';
10
10
  import type { TabBarItemProps } from './types';
11
11
 
12
- export function TabBarItem({ item, compact = false, testID }: TabBarItemProps) {
12
+ export function TabBarItem({ item, compact = false, testID, interactionPolicy }: TabBarItemProps) {
13
13
  const { theme } = useTheme();
14
14
  const active = Boolean(item.active);
15
15
  const disabled = Boolean(item.disabled);
@@ -26,6 +26,7 @@ export function TabBarItem({ item, compact = false, testID }: TabBarItemProps) {
26
26
  accessibilityRole={item.accessibilityRole ?? 'tab'}
27
27
  accessibilityState={resolvedAccessibilityState}
28
28
  disabled={disabled}
29
+ interactionPolicy={interactionPolicy}
29
30
  onPress={item.onPress}
30
31
  style={{ flex: 1 }}
31
32
  testID={resolvedTestID}
@@ -42,3 +42,99 @@ describe('navigation module boundaries', () => {
42
42
  });
43
43
  });
44
44
  });
45
+
46
+ describe('navigation interaction policy', () => {
47
+ it('host component prop types include interactionPolicy', () => {
48
+ const sources = listNavigationSources().map((path) => ({
49
+ path,
50
+ content: readFileSync(path, 'utf8'),
51
+ }));
52
+
53
+ const hostComponents = [{ path: 'types.ts', prop: 'interactionPolicy?: InteractionPolicy' }];
54
+
55
+ sources.forEach(({ content, path }) => {
56
+ const component = hostComponents.find((host) => path.endsWith(host.path));
57
+ if (component) {
58
+ expect(content, path).toContain(component.prop);
59
+ }
60
+ });
61
+ });
62
+
63
+ it('navigation components accept interactionPolicy prop', () => {
64
+ const sources = listNavigationSources().map((path) => ({
65
+ path,
66
+ content: readFileSync(path, 'utf8'),
67
+ }));
68
+
69
+ const components = [
70
+ 'NavigationItem.tsx',
71
+ 'NavigationList.tsx',
72
+ 'TabBar.tsx',
73
+ 'TabBarItem.tsx',
74
+ 'DrawerNavigation.tsx',
75
+ 'DrawerNavigationItem.tsx',
76
+ ];
77
+
78
+ sources.forEach(({ content, path }) => {
79
+ const component = components.find((c) => path.endsWith(c));
80
+ if (component) {
81
+ expect(content, path).toContain('interactionPolicy');
82
+ }
83
+ });
84
+ });
85
+
86
+ it('direct ButtonBase owners forward interactionPolicy unchanged', () => {
87
+ const sources = listNavigationSources().map((path) => ({
88
+ path,
89
+ content: readFileSync(path, 'utf8'),
90
+ }));
91
+
92
+ const directButtonBaseOwners = ['NavigationItem.tsx', 'TabBarItem.tsx'];
93
+
94
+ sources.forEach(({ content, path }) => {
95
+ const component = directButtonBaseOwners.find((owner) => path.endsWith(owner));
96
+ if (component) {
97
+ expect(content, path).toContain('interactionPolicy={interactionPolicy}');
98
+ }
99
+ });
100
+ });
101
+
102
+ it('container components forward interactionPolicy to children', () => {
103
+ const sources = listNavigationSources().map((path) => ({
104
+ path,
105
+ content: readFileSync(path, 'utf8'),
106
+ }));
107
+
108
+ const forwardingMap: Record<string, string> = {
109
+ 'NavigationList.tsx': 'NavigationItem',
110
+ 'TabBar.tsx': 'TabBarItem',
111
+ 'DrawerNavigation.tsx': 'DrawerNavigationItem',
112
+ 'DrawerNavigationItem.tsx': 'NavigationItem',
113
+ };
114
+
115
+ sources.forEach(({ content, path }) => {
116
+ const filename = path.split('/').pop();
117
+ if (filename && forwardingMap[filename]) {
118
+ expect(content, path).toContain(`interactionPolicy={interactionPolicy}`);
119
+ }
120
+ });
121
+ });
122
+
123
+ it('does not map passive interactionPolicy to disabled in navigation components', () => {
124
+ const sources = listNavigationSources().map((path) => ({
125
+ path,
126
+ content: readFileSync(path, 'utf8'),
127
+ }));
128
+
129
+ const passiveToDisabledPatterns = [
130
+ /disabled=\{interactionPolicy === 'passive'\}/,
131
+ /disabled=\{passive/,
132
+ ];
133
+
134
+ sources.forEach(({ content, path }) => {
135
+ passiveToDisabledPatterns.forEach((pattern) => {
136
+ expect(content, path).not.toMatch(pattern);
137
+ });
138
+ });
139
+ });
140
+ });
@@ -1,6 +1,7 @@
1
1
  import type React from 'react';
2
2
  import type { AccessibilityRole, AccessibilityState } from 'react-native';
3
3
 
4
+ import type { InteractionPolicy } from '../../interactionPolicy';
4
5
  import type { IconProps } from '../../primitives/icon';
5
6
 
6
7
  export type NavigationItemIcon = Pick<IconProps, 'name' | 'provider'>;
@@ -23,6 +24,7 @@ export interface NavigationItemProps {
23
24
  item: NavigationItemSpec;
24
25
  compact?: boolean;
25
26
  testID?: string;
27
+ interactionPolicy?: InteractionPolicy;
26
28
  }
27
29
 
28
30
  export interface NavigationListProps {
@@ -30,18 +32,21 @@ export interface NavigationListProps {
30
32
  orientation?: 'vertical' | 'horizontal';
31
33
  compact?: boolean;
32
34
  testID?: string;
35
+ interactionPolicy?: InteractionPolicy;
33
36
  }
34
37
 
35
38
  export interface TabBarProps {
36
39
  items: readonly NavigationItemSpec[];
37
40
  compact?: boolean;
38
41
  testID?: string;
42
+ interactionPolicy?: InteractionPolicy;
39
43
  }
40
44
 
41
45
  export interface TabBarItemProps {
42
46
  item: NavigationItemSpec;
43
47
  compact?: boolean;
44
48
  testID?: string;
49
+ interactionPolicy?: InteractionPolicy;
45
50
  }
46
51
 
47
52
  export interface DrawerNavigationProps {
@@ -50,10 +55,12 @@ export interface DrawerNavigationProps {
50
55
  footer?: React.ReactNode;
51
56
  compact?: boolean;
52
57
  testID?: string;
58
+ interactionPolicy?: InteractionPolicy;
53
59
  }
54
60
 
55
61
  export interface DrawerNavigationItemProps {
56
62
  item: NavigationItemSpec;
57
63
  compact?: boolean;
58
64
  testID?: string;
65
+ interactionPolicy?: InteractionPolicy;
59
66
  }
@@ -7,7 +7,13 @@ import { useTheme } from '../../theme/ThemeContext';
7
7
  import { useTabsContext } from './context';
8
8
  import type { TabProps } from './types';
9
9
 
10
- export function Tab({ value, children, disabled = false, testID }: TabProps) {
10
+ export function Tab({
11
+ value,
12
+ children,
13
+ disabled = false,
14
+ interactionPolicy = 'enabled',
15
+ testID,
16
+ }: TabProps) {
11
17
  const { theme } = useTheme();
12
18
  const {
13
19
  activeValue,
@@ -22,6 +28,7 @@ export function Tab({ value, children, disabled = false, testID }: TabProps) {
22
28
  const selected = activeValue === value;
23
29
  const tabId = getTabId(value);
24
30
  const panelId = getPanelId(value);
31
+ const passive = interactionPolicy === 'passive';
25
32
 
26
33
  React.useEffect(() => {
27
34
  registerTab({
@@ -50,11 +57,15 @@ export function Tab({ value, children, disabled = false, testID }: TabProps) {
50
57
  disabled={disabled}
51
58
  onFocus={() => setFocusedValue(value)}
52
59
  onBlur={() => setFocusedValue(undefined)}
53
- onPress={() => {
54
- if (!disabled) {
55
- setActiveValue(value);
56
- }
57
- }}
60
+ onPress={
61
+ passive
62
+ ? undefined
63
+ : () => {
64
+ if (!disabled) {
65
+ setActiveValue(value);
66
+ }
67
+ }
68
+ }
58
69
  ref={pressableRef}
59
70
  testID={testID}
60
71
  >
@@ -1,5 +1,7 @@
1
1
  import type React from 'react';
2
2
 
3
+ import type { InteractionPolicy } from '../../interactionPolicy';
4
+
3
5
  export interface TabsProps {
4
6
  children?: React.ReactNode;
5
7
  value?: string;
@@ -17,6 +19,7 @@ export interface TabProps {
17
19
  value: string;
18
20
  children?: React.ReactNode;
19
21
  disabled?: boolean;
22
+ interactionPolicy?: InteractionPolicy;
20
23
  testID?: string;
21
24
  }
22
25
 
@@ -22,6 +22,7 @@ export function TextInput({
22
22
  invalid = false,
23
23
  leadingAccessory,
24
24
  trailingAccessory,
25
+ interactionPolicy = 'enabled',
25
26
  style,
26
27
  testID,
27
28
  onFocus,
@@ -52,6 +53,7 @@ export function TextInput({
52
53
  const containerMinHeight = props.multiline
53
54
  ? inputMinHeight + controlSize.paddingVertical * 2
54
55
  : controlSize.minHeight;
56
+ const passive = interactionPolicy === 'passive';
55
57
 
56
58
  const handleFocus: NonNullable<TextInputProps['onFocus']> = (event) => {
57
59
  setFocused(true);
@@ -87,10 +89,16 @@ export function TextInput({
87
89
  <ReactNativeTextInput
88
90
  {...props}
89
91
  defaultValue={defaultValue}
90
- editable={!disabled && !readOnly}
92
+ editable={passive ? false : !disabled && !readOnly}
91
93
  numberOfLines={props.multiline ? props.numberOfLines : 1}
92
94
  onBlur={handleBlur}
93
- onChangeText={onChangeText}
95
+ onChangeText={(nextValue) => {
96
+ if (passive) {
97
+ return;
98
+ }
99
+
100
+ onChangeText?.(nextValue);
101
+ }}
94
102
  onFocus={handleFocus}
95
103
  placeholder={placeholder}
96
104
  placeholderTextColor={colors.placeholderColor}
@@ -5,6 +5,7 @@ import type {
5
5
  TextStyle,
6
6
  } from 'react-native';
7
7
 
8
+ import type { InteractionPolicy } from '../../interactionPolicy';
8
9
  import type { ControlSize } from '../../internal/resolvers/resolveControlSize';
9
10
 
10
11
  export interface TextInputProps extends Omit<
@@ -27,6 +28,7 @@ export interface TextInputProps extends Omit<
27
28
  invalid?: boolean;
28
29
  leadingAccessory?: React.ReactNode;
29
30
  trailingAccessory?: React.ReactNode;
31
+ interactionPolicy?: InteractionPolicy;
30
32
  style?: StyleProp<TextStyle>;
31
33
  testID?: string;
32
34
  }
@@ -38,10 +38,18 @@ function resolveToastStatusTextColor(status: ToastProps['status']): SurfaceColor
38
38
  }
39
39
  }
40
40
 
41
- export function Toast({ title, description, status = 'default', onDismiss, testID }: ToastProps) {
41
+ export function Toast({
42
+ title,
43
+ description,
44
+ status = 'default',
45
+ onDismiss,
46
+ interactionPolicy = 'enabled',
47
+ testID,
48
+ }: ToastProps) {
42
49
  const { theme } = useTheme();
43
50
  const statusColor = resolveToastStatusColor(theme, status);
44
51
  const statusTextColor = resolveToastStatusTextColor(status);
52
+ const passive = interactionPolicy === 'passive';
45
53
 
46
54
  return (
47
55
  <Surface
@@ -70,7 +78,7 @@ export function Toast({ title, description, status = 'default', onDismiss, testI
70
78
  <Pressable
71
79
  accessibilityLabel="Dismiss notification"
72
80
  accessibilityRole="button"
73
- onPress={onDismiss}
81
+ onPress={passive ? undefined : onDismiss}
74
82
  testID={testID ? `${testID}-dismiss` : undefined}
75
83
  >
76
84
  <Text color={statusTextColor}>×</Text>
@@ -1,5 +1,6 @@
1
1
  import type React from 'react';
2
2
 
3
+ import type { InteractionPolicy } from '../../interactionPolicy';
3
4
  import type { SurfaceStatusColor } from '../../surfaceColor';
4
5
 
5
6
  export type ToastStatus = 'default' | SurfaceStatusColor;
@@ -9,6 +10,7 @@ export interface ToastProps {
9
10
  description?: React.ReactNode;
10
11
  status?: ToastStatus;
11
12
  onDismiss?: (() => void) | undefined;
13
+ interactionPolicy?: InteractionPolicy;
12
14
  testID?: string;
13
15
  }
14
16
 
@@ -25,6 +25,7 @@ export function Tooltip({
25
25
  children,
26
26
  content,
27
27
  delay = 150,
28
+ interactionPolicy = 'enabled',
28
29
  placement = 'top',
29
30
  testID,
30
31
  }: TooltipProps) {
@@ -34,6 +35,7 @@ export function Tooltip({
34
35
  const timeoutRef = React.useRef<ReturnType<typeof setTimeout> | null>(null);
35
36
  const [visible, setVisible] = React.useState(false);
36
37
  const [layout, setLayout] = React.useState<LayoutRectangle | null>(null);
38
+ const passive = interactionPolicy === 'passive';
37
39
 
38
40
  const show = React.useCallback(() => {
39
41
  if (timeoutRef.current) {
@@ -74,10 +76,10 @@ export function Tooltip({
74
76
  return (
75
77
  <View collapsable={false} ref={anchorRef} testID={testID ? `${testID}-anchor` : undefined}>
76
78
  <Pressable
77
- onBlur={hide}
78
- onFocus={show}
79
- onHoverIn={Platform.OS === 'web' ? show : undefined}
80
- onHoverOut={Platform.OS === 'web' ? hide : undefined}
79
+ onBlur={passive ? undefined : hide}
80
+ onFocus={passive ? undefined : show}
81
+ onHoverIn={Platform.OS === 'web' && !passive ? show : undefined}
82
+ onHoverOut={Platform.OS === 'web' && !passive ? hide : undefined}
81
83
  >
82
84
  {children}
83
85
  </Pressable>
@@ -1,9 +1,12 @@
1
1
  import type React from 'react';
2
2
 
3
+ import type { InteractionPolicy } from '../../interactionPolicy';
4
+
3
5
  export interface TooltipProps {
4
6
  children?: React.ReactNode;
5
7
  content?: React.ReactNode;
6
8
  delay?: number;
9
+ interactionPolicy?: InteractionPolicy;
7
10
  placement?: 'top' | 'bottom';
8
11
  testID?: string;
9
12
  }
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 type { InteractionPolicy, InteractionPolicyProps } from './interactionPolicy';",
22
23
  "export { Label } from './components/label';",
23
24
  "export { ListItem } from './components/list-item';",
24
25
  "export {\n DrawerNavigation,\n DrawerNavigationItem,\n NavigationItem,\n NavigationList,\n TabBar,\n TabBarItem,\n} from './components/navigation';",
package/src/index.ts CHANGED
@@ -61,6 +61,7 @@ export { FontProvider, useFontContext } from './context/FontContext';
61
61
  export type { I18nInstance, TranslationRuntime, Translator } from './context/TranslationContext';
62
62
  export { TranslationProvider, useTranslationContext } from './context/TranslationContext';
63
63
  export * from './core/responsive';
64
+ export type { InteractionPolicy, InteractionPolicyProps } from './interactionPolicy';
64
65
  export * from './layout';
65
66
  export type { ButtonBaseProps } from './primitives/button-base';
66
67
  export { ButtonBase } from './primitives/button-base';
@@ -0,0 +1,86 @@
1
+ import { readFileSync } from 'node:fs';
2
+
3
+ import { describe, expect, it } from 'bun:test';
4
+
5
+ const indexSource = readFileSync(new URL('./index.ts', import.meta.url), 'utf8');
6
+
7
+ describe('InteractionPolicy public export', () => {
8
+ it('exports InteractionPolicy from the public root', () => {
9
+ expect(indexSource).toContain(
10
+ "export type { InteractionPolicy, InteractionPolicyProps } from './interactionPolicy';",
11
+ );
12
+ });
13
+
14
+ it('exports InteractionPolicyProps from the public root', () => {
15
+ expect(indexSource).toContain(
16
+ "export type { InteractionPolicy, InteractionPolicyProps } from './interactionPolicy';",
17
+ );
18
+ });
19
+
20
+ it('does not export InteractionPolicy from button-base', () => {
21
+ expect(indexSource).not.toContain(
22
+ "export type { InteractionPolicy, InteractionPolicyProps } from './primitives/button-base';",
23
+ );
24
+ });
25
+ });
26
+
27
+ describe('InteractionPolicy type', () => {
28
+ it('only allows enabled', () => {
29
+ const policy = 'enabled' as const;
30
+ expect(policy).toBe('enabled');
31
+ });
32
+
33
+ it('only allows passive', () => {
34
+ const policy = 'passive' as const;
35
+ expect(policy).toBe('passive');
36
+ });
37
+ });
38
+
39
+ describe('InteractionPolicyProps', () => {
40
+ it('allows omitted interactionPolicy', () => {
41
+ const props: { readonly interactionPolicy?: 'enabled' | 'passive' } = {};
42
+ expect(props.interactionPolicy).toBeUndefined();
43
+ });
44
+
45
+ it('allows explicit enabled', () => {
46
+ const props: { readonly interactionPolicy?: 'enabled' | 'passive' } = {
47
+ interactionPolicy: 'enabled',
48
+ };
49
+ expect(props.interactionPolicy).toBe('enabled');
50
+ });
51
+
52
+ it('allows explicit passive', () => {
53
+ const props: { readonly interactionPolicy?: 'enabled' | 'passive' } = {
54
+ interactionPolicy: 'passive',
55
+ };
56
+ expect(props.interactionPolicy).toBe('passive');
57
+ });
58
+ });
59
+
60
+ describe('ButtonBaseProps includes interactionPolicy', () => {
61
+ it('has interactionPolicy in ButtonBaseProps', () => {
62
+ expect(indexSource).toContain(
63
+ "export type { ButtonBaseProps } from './primitives/button-base';",
64
+ );
65
+ });
66
+ });
67
+
68
+ describe('Policy does not leak to native hosts', () => {
69
+ it('InteractionPolicy is not a native View prop', () => {
70
+ const viewProps: Record<string, unknown> = {
71
+ accessible: true,
72
+ accessibilityLabel: 'test',
73
+ };
74
+ expect(viewProps.interactionPolicy).toBeUndefined();
75
+ });
76
+
77
+ it('InteractionPolicy is not a native TextInput prop', () => {
78
+ const textInputProps: Record<string, unknown> = {
79
+ editable: true,
80
+ onChangeText: (_text: string) => {
81
+ void _text;
82
+ },
83
+ };
84
+ expect(textInputProps.interactionPolicy).toBeUndefined();
85
+ });
86
+ });
@@ -0,0 +1,5 @@
1
+ export type InteractionPolicy = 'enabled' | 'passive';
2
+
3
+ export interface InteractionPolicyProps {
4
+ interactionPolicy?: InteractionPolicy;
5
+ }
@@ -28,6 +28,7 @@ function getInteractionState(
28
28
  export function ButtonBase({
29
29
  children,
30
30
  disabled = false,
31
+ interactionPolicy = 'enabled',
31
32
  onPress,
32
33
  onLongPress,
33
34
  accessibilityLabel,
@@ -43,6 +44,7 @@ export function ButtonBase({
43
44
  const [focused, setFocused] = React.useState(false);
44
45
  const isWeb = Platform.OS === 'web';
45
46
  const resolvedBoxStyles = resolveBoxStyles(theme, breakpoint, props);
47
+ const passive = interactionPolicy === 'passive';
46
48
 
47
49
  return (
48
50
  <Pressable
@@ -58,8 +60,8 @@ export function ButtonBase({
58
60
  }}
59
61
  onHoverIn={isWeb ? () => setHovered(true) : undefined}
60
62
  onHoverOut={isWeb ? () => setHovered(false) : undefined}
61
- onLongPress={onLongPress}
62
- onPress={onPress}
63
+ onLongPress={passive ? undefined : onLongPress}
64
+ onPress={passive ? undefined : onPress}
63
65
  style={(pressableState) => {
64
66
  const state = getInteractionState(pressableState, hovered, focused, disabled);
65
67
 
@@ -1,10 +1,12 @@
1
1
  import type React from 'react';
2
2
  import type { AccessibilityRole, AccessibilityState, GestureResponderEvent } from 'react-native';
3
3
 
4
+ import type { InteractionPolicyProps } from '../../interactionPolicy';
4
5
  import type { InteractionState } from '../../internal/resolvers/resolveInteractiveState';
5
6
  import type { BoxProps } from '../../layout';
6
7
 
7
- export interface ButtonBaseProps extends Omit<BoxProps, 'children' | 'pointerEvents'> {
8
+ export interface ButtonBaseProps
9
+ extends Omit<BoxProps, 'children' | 'pointerEvents'>, InteractionPolicyProps {
8
10
  children?: React.ReactNode | ((state: InteractionState) => React.ReactNode);
9
11
  disabled?: boolean;
10
12
  onPress?: ((event: GestureResponderEvent) => void) | undefined;