@ankhorage/surface 4.4.1 → 5.0.1

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 (235) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/README.md +2 -2
  3. package/dist/features/bottom-sheet/BottomSheetProvider.js +8 -6
  4. package/dist/features/bottom-sheet/BottomSheetProvider.js.map +1 -1
  5. package/dist/features/bottom-sheet/public.d.ts +2 -1
  6. package/dist/features/bottom-sheet/public.d.ts.map +1 -1
  7. package/dist/features/bottom-sheet/public.js +1 -0
  8. package/dist/features/bottom-sheet/public.js.map +1 -1
  9. package/dist/{components/modal → features/modal/adapters/inbound}/Modal.d.ts +2 -1
  10. package/dist/features/modal/adapters/inbound/Modal.d.ts.map +1 -0
  11. package/dist/features/modal/adapters/inbound/Modal.js +51 -0
  12. package/dist/features/modal/adapters/inbound/Modal.js.map +1 -0
  13. package/dist/features/modal/public.d.ts +3 -0
  14. package/dist/features/modal/public.d.ts.map +1 -0
  15. package/dist/features/modal/public.js +2 -0
  16. package/dist/features/modal/public.js.map +1 -0
  17. package/dist/features/popover-menu/adapters/inbound/PopoverMenu.d.ts +5 -0
  18. package/dist/features/popover-menu/adapters/inbound/PopoverMenu.d.ts.map +1 -0
  19. package/dist/features/popover-menu/adapters/inbound/PopoverMenu.js +85 -0
  20. package/dist/features/popover-menu/adapters/inbound/PopoverMenu.js.map +1 -0
  21. package/dist/features/popover-menu/composition/usePopoverMenuController.d.ts +19 -0
  22. package/dist/features/popover-menu/composition/usePopoverMenuController.d.ts.map +1 -0
  23. package/dist/features/popover-menu/composition/usePopoverMenuController.js +35 -0
  24. package/dist/features/popover-menu/composition/usePopoverMenuController.js.map +1 -0
  25. package/dist/features/popover-menu/public.d.ts +3 -0
  26. package/dist/features/popover-menu/public.d.ts.map +1 -0
  27. package/dist/features/popover-menu/public.js +2 -0
  28. package/dist/features/popover-menu/public.js.map +1 -0
  29. package/dist/features/popover-menu/utils/resolveNextMenuIndex.d.ts +5 -0
  30. package/dist/features/popover-menu/utils/resolveNextMenuIndex.d.ts.map +1 -0
  31. package/dist/features/popover-menu/utils/resolveNextMenuIndex.js +18 -0
  32. package/dist/features/popover-menu/utils/resolveNextMenuIndex.js.map +1 -0
  33. package/dist/features/tabs/adapters/inbound/Tab.d.ts +5 -0
  34. package/dist/features/tabs/adapters/inbound/Tab.d.ts.map +1 -0
  35. package/dist/features/tabs/adapters/inbound/Tab.js +33 -0
  36. package/dist/features/tabs/adapters/inbound/Tab.js.map +1 -0
  37. package/dist/features/tabs/adapters/inbound/TabList.d.ts +5 -0
  38. package/dist/features/tabs/adapters/inbound/TabList.d.ts.map +1 -0
  39. package/dist/features/tabs/adapters/inbound/TabList.js +42 -0
  40. package/dist/features/tabs/adapters/inbound/TabList.js.map +1 -0
  41. package/dist/features/tabs/adapters/inbound/TabPanel.d.ts +5 -0
  42. package/dist/features/tabs/adapters/inbound/TabPanel.d.ts.map +1 -0
  43. package/dist/features/tabs/adapters/inbound/TabPanel.js +14 -0
  44. package/dist/features/tabs/adapters/inbound/TabPanel.js.map +1 -0
  45. package/dist/features/tabs/adapters/inbound/Tabs.d.ts +5 -0
  46. package/dist/features/tabs/adapters/inbound/Tabs.d.ts.map +1 -0
  47. package/dist/features/tabs/adapters/inbound/Tabs.js +12 -0
  48. package/dist/features/tabs/adapters/inbound/Tabs.js.map +1 -0
  49. package/dist/features/tabs/composition/TabsContext.d.ts +4 -0
  50. package/dist/features/tabs/composition/TabsContext.d.ts.map +1 -0
  51. package/dist/features/tabs/composition/TabsContext.js +3 -0
  52. package/dist/features/tabs/composition/TabsContext.js.map +1 -0
  53. package/dist/features/tabs/composition/useTabRegistration.d.ts +7 -0
  54. package/dist/features/tabs/composition/useTabRegistration.d.ts.map +1 -0
  55. package/dist/features/tabs/composition/useTabRegistration.js +20 -0
  56. package/dist/features/tabs/composition/useTabRegistration.js.map +1 -0
  57. package/dist/features/tabs/composition/useTabsContext.d.ts +4 -0
  58. package/dist/features/tabs/composition/useTabsContext.d.ts.map +1 -0
  59. package/dist/{components/tabs/context.js → features/tabs/composition/useTabsContext.js} +4 -4
  60. package/dist/features/tabs/composition/useTabsContext.js.map +1 -0
  61. package/dist/features/tabs/composition/useTabsController.d.ts +4 -0
  62. package/dist/features/tabs/composition/useTabsController.d.ts.map +1 -0
  63. package/dist/features/tabs/composition/useTabsController.js +43 -0
  64. package/dist/features/tabs/composition/useTabsController.js.map +1 -0
  65. package/dist/features/tabs/public.d.ts +6 -0
  66. package/dist/features/tabs/public.d.ts.map +1 -0
  67. package/dist/features/tabs/public.js +5 -0
  68. package/dist/features/tabs/public.js.map +1 -0
  69. package/dist/features/tabs/utils/resolveNextTabValue.d.ts +4 -0
  70. package/dist/features/tabs/utils/resolveNextTabValue.d.ts.map +1 -0
  71. package/dist/{components/tabs/navigation.js → features/tabs/utils/resolveNextTabValue.js} +10 -13
  72. package/dist/features/tabs/utils/resolveNextTabValue.js.map +1 -0
  73. package/dist/features/tabs/utils/resolveTabElementId.d.ts +3 -0
  74. package/dist/features/tabs/utils/resolveTabElementId.d.ts.map +1 -0
  75. package/dist/features/tabs/utils/resolveTabElementId.js +10 -0
  76. package/dist/features/tabs/utils/resolveTabElementId.js.map +1 -0
  77. package/dist/features/tooltip/adapters/inbound/Tooltip.d.ts +5 -0
  78. package/dist/features/tooltip/adapters/inbound/Tooltip.d.ts.map +1 -0
  79. package/dist/features/tooltip/adapters/inbound/Tooltip.js +25 -0
  80. package/dist/features/tooltip/adapters/inbound/Tooltip.js.map +1 -0
  81. package/dist/features/tooltip/composition/useTooltipVisibility.d.ts +9 -0
  82. package/dist/features/tooltip/composition/useTooltipVisibility.d.ts.map +1 -0
  83. package/dist/features/tooltip/composition/useTooltipVisibility.js +25 -0
  84. package/dist/features/tooltip/composition/useTooltipVisibility.js.map +1 -0
  85. package/dist/features/tooltip/public.d.ts +3 -0
  86. package/dist/features/tooltip/public.d.ts.map +1 -0
  87. package/dist/features/tooltip/public.js +2 -0
  88. package/dist/features/tooltip/public.js.map +1 -0
  89. package/dist/index.d.ts +8 -8
  90. package/dist/index.d.ts.map +1 -1
  91. package/dist/index.js +4 -4
  92. package/dist/index.js.map +1 -1
  93. package/dist/types/bottomSheet.d.ts +3 -0
  94. package/dist/types/bottomSheet.d.ts.map +1 -1
  95. package/dist/types/bottomSheet.js.map +1 -1
  96. package/dist/{components/modal/types.d.ts → types/modal.d.ts} +2 -2
  97. package/dist/types/modal.d.ts.map +1 -0
  98. package/dist/types/modal.js +2 -0
  99. package/dist/types/modal.js.map +1 -0
  100. package/dist/types/popoverMenu.d.ts +24 -0
  101. package/dist/types/popoverMenu.d.ts.map +1 -0
  102. package/dist/types/popoverMenu.js +2 -0
  103. package/dist/types/popoverMenu.js.map +1 -0
  104. package/dist/types/tabs.d.ts +43 -0
  105. package/dist/types/tabs.d.ts.map +1 -0
  106. package/dist/types/tabs.js +2 -0
  107. package/dist/types/tabs.js.map +1 -0
  108. package/dist/{components/tooltip/types.d.ts → types/tooltip.d.ts} +2 -2
  109. package/dist/types/tooltip.d.ts.map +1 -0
  110. package/dist/types/tooltip.js +2 -0
  111. package/dist/types/tooltip.js.map +1 -0
  112. package/package.json +1 -1
  113. package/src/features/bottom-sheet/BottomSheetProvider.tsx +13 -8
  114. package/src/features/bottom-sheet/public.test.ts +23 -0
  115. package/src/features/bottom-sheet/public.ts +12 -1
  116. package/src/features/modal/adapters/inbound/Modal.tsx +74 -0
  117. package/src/features/modal/public.ts +2 -0
  118. package/src/features/popover-menu/adapters/inbound/PopoverMenu.tsx +201 -0
  119. package/src/features/popover-menu/composition/usePopoverMenuController.ts +55 -0
  120. package/src/features/popover-menu/public.test.ts +13 -0
  121. package/src/features/popover-menu/public.ts +6 -0
  122. package/src/{components/menu/navigation.test.ts → features/popover-menu/utils/resolveNextMenuIndex.test.ts} +4 -4
  123. package/src/features/popover-menu/utils/resolveNextMenuIndex.ts +21 -0
  124. package/src/features/tabs/adapters/inbound/Tab.tsx +67 -0
  125. package/src/features/tabs/adapters/inbound/TabList.tsx +50 -0
  126. package/src/{components/tabs → features/tabs/adapters/inbound}/TabPanel.tsx +6 -10
  127. package/src/features/tabs/adapters/inbound/Tabs.tsx +17 -0
  128. package/src/features/tabs/composition/TabsContext.tsx +5 -0
  129. package/src/features/tabs/composition/useTabRegistration.ts +27 -0
  130. package/src/features/tabs/composition/useTabsContext.ts +13 -0
  131. package/src/features/tabs/composition/useTabsController.ts +55 -0
  132. package/src/features/tabs/public.ts +5 -0
  133. package/src/{components/tabs/navigation.test.ts → features/tabs/utils/resolveNextTabValue.test.ts} +2 -2
  134. package/src/features/tabs/utils/resolveNextTabValue.ts +24 -0
  135. package/src/features/tabs/utils/resolveTabElementId.test.ts +19 -0
  136. package/src/features/tabs/utils/resolveTabElementId.ts +14 -0
  137. package/src/features/tooltip/adapters/inbound/Tooltip.tsx +56 -0
  138. package/src/features/tooltip/composition/useTooltipVisibility.ts +29 -0
  139. package/src/features/tooltip/public.test.ts +11 -0
  140. package/src/features/tooltip/public.ts +2 -0
  141. package/src/index.test.ts +12 -5
  142. package/src/index.ts +12 -8
  143. package/src/types/bottomSheet.ts +4 -0
  144. package/src/{components/modal/types.ts → types/modal.ts} +1 -1
  145. package/src/types/popoverMenu.ts +27 -0
  146. package/src/types/tabs.ts +51 -0
  147. package/src/{components/tooltip/types.ts → types/tooltip.ts} +1 -1
  148. package/dist/components/menu/Menu.d.ts +0 -4
  149. package/dist/components/menu/Menu.d.ts.map +0 -1
  150. package/dist/components/menu/Menu.js +0 -152
  151. package/dist/components/menu/Menu.js.map +0 -1
  152. package/dist/components/menu/index.d.ts +0 -3
  153. package/dist/components/menu/index.d.ts.map +0 -1
  154. package/dist/components/menu/index.js +0 -2
  155. package/dist/components/menu/index.js.map +0 -1
  156. package/dist/components/menu/navigation.d.ts +0 -4
  157. package/dist/components/menu/navigation.d.ts.map +0 -1
  158. package/dist/components/menu/navigation.js +0 -25
  159. package/dist/components/menu/navigation.js.map +0 -1
  160. package/dist/components/menu/types.d.ts +0 -23
  161. package/dist/components/menu/types.d.ts.map +0 -1
  162. package/dist/components/menu/types.js +0 -2
  163. package/dist/components/menu/types.js.map +0 -1
  164. package/dist/components/modal/Modal.d.ts.map +0 -1
  165. package/dist/components/modal/Modal.js +0 -59
  166. package/dist/components/modal/Modal.js.map +0 -1
  167. package/dist/components/modal/index.d.ts +0 -3
  168. package/dist/components/modal/index.d.ts.map +0 -1
  169. package/dist/components/modal/index.js +0 -2
  170. package/dist/components/modal/index.js.map +0 -1
  171. package/dist/components/modal/types.d.ts.map +0 -1
  172. package/dist/components/modal/types.js +0 -2
  173. package/dist/components/modal/types.js.map +0 -1
  174. package/dist/components/tabs/Tab.d.ts +0 -4
  175. package/dist/components/tabs/Tab.d.ts.map +0 -1
  176. package/dist/components/tabs/Tab.js +0 -48
  177. package/dist/components/tabs/Tab.js.map +0 -1
  178. package/dist/components/tabs/TabList.d.ts +0 -4
  179. package/dist/components/tabs/TabList.d.ts.map +0 -1
  180. package/dist/components/tabs/TabList.js +0 -37
  181. package/dist/components/tabs/TabList.js.map +0 -1
  182. package/dist/components/tabs/TabPanel.d.ts +0 -4
  183. package/dist/components/tabs/TabPanel.d.ts.map +0 -1
  184. package/dist/components/tabs/TabPanel.js +0 -16
  185. package/dist/components/tabs/TabPanel.js.map +0 -1
  186. package/dist/components/tabs/Tabs.d.ts +0 -4
  187. package/dist/components/tabs/Tabs.d.ts.map +0 -1
  188. package/dist/components/tabs/Tabs.js +0 -54
  189. package/dist/components/tabs/Tabs.js.map +0 -1
  190. package/dist/components/tabs/a11y.d.ts +0 -3
  191. package/dist/components/tabs/a11y.d.ts.map +0 -1
  192. package/dist/components/tabs/a11y.js +0 -13
  193. package/dist/components/tabs/a11y.js.map +0 -1
  194. package/dist/components/tabs/context.d.ts +0 -20
  195. package/dist/components/tabs/context.d.ts.map +0 -1
  196. package/dist/components/tabs/context.js.map +0 -1
  197. package/dist/components/tabs/index.d.ts +0 -6
  198. package/dist/components/tabs/index.d.ts.map +0 -1
  199. package/dist/components/tabs/index.js +0 -5
  200. package/dist/components/tabs/index.js.map +0 -1
  201. package/dist/components/tabs/navigation.d.ts +0 -3
  202. package/dist/components/tabs/navigation.d.ts.map +0 -1
  203. package/dist/components/tabs/navigation.js.map +0 -1
  204. package/dist/components/tabs/types.d.ts +0 -26
  205. package/dist/components/tabs/types.d.ts.map +0 -1
  206. package/dist/components/tabs/types.js +0 -2
  207. package/dist/components/tabs/types.js.map +0 -1
  208. package/dist/components/tooltip/Tooltip.d.ts +0 -4
  209. package/dist/components/tooltip/Tooltip.d.ts.map +0 -1
  210. package/dist/components/tooltip/Tooltip.js +0 -75
  211. package/dist/components/tooltip/Tooltip.js.map +0 -1
  212. package/dist/components/tooltip/index.d.ts +0 -3
  213. package/dist/components/tooltip/index.d.ts.map +0 -1
  214. package/dist/components/tooltip/index.js +0 -2
  215. package/dist/components/tooltip/index.js.map +0 -1
  216. package/dist/components/tooltip/types.d.ts.map +0 -1
  217. package/dist/components/tooltip/types.js +0 -2
  218. package/dist/components/tooltip/types.js.map +0 -1
  219. package/src/components/menu/Menu.tsx +0 -228
  220. package/src/components/menu/index.ts +0 -2
  221. package/src/components/menu/navigation.ts +0 -34
  222. package/src/components/menu/types.ts +0 -26
  223. package/src/components/modal/Modal.tsx +0 -96
  224. package/src/components/modal/index.ts +0 -2
  225. package/src/components/tabs/Tab.tsx +0 -89
  226. package/src/components/tabs/TabList.tsx +0 -51
  227. package/src/components/tabs/Tabs.tsx +0 -67
  228. package/src/components/tabs/a11y.test.ts +0 -15
  229. package/src/components/tabs/a11y.ts +0 -15
  230. package/src/components/tabs/context.tsx +0 -31
  231. package/src/components/tabs/index.ts +0 -5
  232. package/src/components/tabs/navigation.ts +0 -32
  233. package/src/components/tabs/types.ts +0 -30
  234. package/src/components/tooltip/Tooltip.tsx +0 -117
  235. package/src/components/tooltip/index.ts +0 -2
@@ -0,0 +1,21 @@
1
+ /*** Resolves the next enabled action index for standard menu keyboard navigation. */
2
+ export function resolveNextMenuIndex(
3
+ items: readonly { disabled?: boolean }[],
4
+ currentIndex: number,
5
+ key: string,
6
+ ): number {
7
+ const enabledIndexes = items.flatMap((item, index) => (item.disabled ? [] : [index]));
8
+ if (enabledIndexes.length === 0) return -1;
9
+ if (key === 'Home') return enabledIndexes.at(0) ?? -1;
10
+ if (key === 'End') return enabledIndexes.at(-1) ?? -1;
11
+
12
+ const activeEnabledIndex = enabledIndexes.indexOf(currentIndex);
13
+ const nextEnabledIndex =
14
+ activeEnabledIndex === -1
15
+ ? 0
16
+ : key === 'ArrowUp'
17
+ ? (activeEnabledIndex - 1 + enabledIndexes.length) % enabledIndexes.length
18
+ : (activeEnabledIndex + 1) % enabledIndexes.length;
19
+
20
+ return enabledIndexes.at(nextEnabledIndex) ?? -1;
21
+ }
@@ -0,0 +1,67 @@
1
+ import React from 'react';
2
+ import { Pressable, type ViewStyle } from 'react-native';
3
+
4
+ import { useTheme } from '../../../../theme/ThemeContext';
5
+ import type { TabProps } from '../../../../types/tabs';
6
+ import { Box } from '../../../layout/public';
7
+ import { Text } from '../../../typography/public';
8
+ import { useTabRegistration } from '../../composition/useTabRegistration';
9
+ import { useTabsContext } from '../../composition/useTabsContext';
10
+
11
+ /*** Renders one accessible selectable tab inside a Tabs context. */
12
+ export function Tab({
13
+ value,
14
+ children,
15
+ disabled = false,
16
+ interactionPolicy = 'enabled',
17
+ testID,
18
+ }: TabProps) {
19
+ const { theme } = useTheme();
20
+ const { activeValue, getPanelId, getTabId, setActiveValue, setFocusedValue } = useTabsContext();
21
+ const pressableRef = useTabRegistration({ disabled, value });
22
+ const selected = activeValue === value;
23
+ const passive = interactionPolicy === 'passive';
24
+ const onPress = passive || disabled ? undefined : () => setActiveValue(value);
25
+
26
+ return (
27
+ <Pressable
28
+ accessibilityLabel={undefined}
29
+ accessibilityRole="tab"
30
+ accessibilityState={{ disabled, selected }}
31
+ aria-controls={getPanelId(value)}
32
+ disabled={disabled}
33
+ nativeID={getTabId(value)}
34
+ onBlur={() => setFocusedValue(undefined)}
35
+ onFocus={() => setFocusedValue(value)}
36
+ onPress={onPress}
37
+ ref={pressableRef}
38
+ testID={testID}
39
+ >
40
+ <Box
41
+ px="m"
42
+ py="s"
43
+ style={resolveTabStyle({
44
+ borderColor: selected
45
+ ? theme.semantics.action.primary.base
46
+ : theme.semantics.border.default,
47
+ disabled,
48
+ })}
49
+ >
50
+ <Text color={selected ? 'primary' : undefined} variant="label" weight="medium">
51
+ {children}
52
+ </Text>
53
+ </Box>
54
+ </Pressable>
55
+ );
56
+ }
57
+
58
+ /*** Resolves active and disabled visual state without inline JSX styles. */
59
+ function resolveTabStyle({
60
+ borderColor,
61
+ disabled,
62
+ }: {
63
+ borderColor: string;
64
+ disabled: boolean;
65
+ }): ViewStyle {
66
+ return { borderBottomColor: borderColor, borderBottomWidth: 2, opacity: disabled ? 0.64 : 1 };
67
+ }
@@ -0,0 +1,50 @@
1
+ import React from 'react';
2
+ import { StyleSheet } from 'react-native';
3
+
4
+ import { useFocusManager } from '../../../../internal/focus/useFocusManager';
5
+ import type { TabListProps, TabNavigationKey } from '../../../../types/tabs';
6
+ import { Box } from '../../../layout/public';
7
+ import { useTabsContext } from '../../composition/useTabsContext';
8
+ import { resolveNextTabValue } from '../../utils/resolveNextTabValue';
9
+
10
+ const navigationKeys: readonly string[] = [
11
+ 'ArrowLeft',
12
+ 'ArrowRight',
13
+ 'ArrowUp',
14
+ 'ArrowDown',
15
+ 'Home',
16
+ 'End',
17
+ ];
18
+
19
+ /*** Renders the accessible tab list and owns keyboard focus navigation. */
20
+ export function TabList({ children, testID }: TabListProps) {
21
+ const { bindKeydown } = useFocusManager();
22
+ const { focusedValue, setActiveValue, tabs } = useTabsContext();
23
+
24
+ React.useEffect(() => {
25
+ if (!focusedValue) return undefined;
26
+
27
+ return bindKeydown((event) => {
28
+ if (!isTabNavigationKey(event.key)) return;
29
+ const nextValue = resolveNextTabValue(tabs, focusedValue, event.key);
30
+ if (!nextValue) return;
31
+
32
+ event.preventDefault();
33
+ tabs.find((tab) => tab.value === nextValue)?.focus();
34
+ setActiveValue(nextValue);
35
+ });
36
+ }, [bindKeydown, focusedValue, setActiveValue, tabs]);
37
+
38
+ return (
39
+ <Box accessibilityRole="tablist" style={styles.list} testID={testID}>
40
+ {children}
41
+ </Box>
42
+ );
43
+ }
44
+
45
+ /*** Narrows keyboard event keys to the tab navigation key contract. */
46
+ function isTabNavigationKey(key: string | undefined): key is TabNavigationKey {
47
+ return key !== undefined && navigationKeys.includes(key);
48
+ }
49
+
50
+ const styles = StyleSheet.create({ list: { flexDirection: 'row' } });
@@ -1,26 +1,22 @@
1
1
  import React from 'react';
2
2
  import { View } from 'react-native';
3
3
 
4
- import { useTabsContext } from './context';
5
- import type { TabPanelProps } from './types';
4
+ import type { TabPanelProps } from '../../../../types/tabs';
5
+ import { useTabsContext } from '../../composition/useTabsContext';
6
6
 
7
7
  const TAB_PANEL_ROLE = 'tabpanel' as React.ComponentProps<typeof View>['accessibilityRole'];
8
8
 
9
+ /*** Renders the content panel associated with the active tab value. */
9
10
  export function TabPanel({ value, children, testID }: TabPanelProps) {
10
11
  const { activeValue, getPanelId, getTabId } = useTabsContext();
11
12
 
12
- if (activeValue !== value) {
13
- return null;
14
- }
15
-
16
- const tabId = getTabId(value);
17
- const panelId = getPanelId(value);
13
+ if (activeValue !== value) return null;
18
14
 
19
15
  return (
20
16
  <View
21
- accessibilityLabelledBy={tabId}
17
+ accessibilityLabelledBy={getTabId(value)}
22
18
  accessibilityRole={TAB_PANEL_ROLE}
23
- nativeID={panelId}
19
+ nativeID={getPanelId(value)}
24
20
  testID={testID}
25
21
  >
26
22
  {children}
@@ -0,0 +1,17 @@
1
+ import React from 'react';
2
+
3
+ import type { TabsProps } from '../../../../types/tabs';
4
+ import { Box } from '../../../layout/public';
5
+ import { TabsContext } from '../../composition/TabsContext';
6
+ import { useTabsController } from '../../composition/useTabsController';
7
+
8
+ /*** Provides accessible tab selection state to TabList, Tab, and TabPanel children. */
9
+ export function Tabs({ children, ...props }: TabsProps) {
10
+ const contextValue = useTabsController(props);
11
+
12
+ return (
13
+ <TabsContext value={contextValue}>
14
+ <Box testID={props.testID}>{children}</Box>
15
+ </TabsContext>
16
+ );
17
+ }
@@ -0,0 +1,5 @@
1
+ import React from 'react';
2
+
3
+ import type { TabsContextValue } from '../../../types/tabs';
4
+
5
+ export const TabsContext = React.createContext<TabsContextValue | null>(null);
@@ -0,0 +1,27 @@
1
+ import React from 'react';
2
+ import type { Pressable } from 'react-native';
3
+
4
+ import { useTabsContext } from './useTabsContext';
5
+
6
+ /*** Registers one focusable tab with the owning Tabs context. */
7
+ export function useTabRegistration({ disabled, value }: { disabled: boolean; value: string }) {
8
+ const { registerTab, unregisterTab } = useTabsContext();
9
+ const pressableRef = React.useRef<React.ElementRef<typeof Pressable> | null>(null);
10
+
11
+ React.useEffect(() => {
12
+ registerTab({
13
+ disabled,
14
+ focus: () => {
15
+ const focusable = pressableRef.current as unknown as {
16
+ focus?: (() => void) | undefined;
17
+ } | null;
18
+ focusable?.focus?.();
19
+ },
20
+ value,
21
+ });
22
+
23
+ return () => unregisterTab(value);
24
+ }, [disabled, registerTab, unregisterTab, value]);
25
+
26
+ return pressableRef;
27
+ }
@@ -0,0 +1,13 @@
1
+ import React from 'react';
2
+
3
+ import type { TabsContextValue } from '../../../types/tabs';
4
+ import { TabsContext } from './TabsContext';
5
+
6
+ /*** Returns the active Tabs feature context. */
7
+ export function useTabsContext(): TabsContextValue {
8
+ const value = React.use(TabsContext);
9
+
10
+ if (!value) throw new Error('Tabs components must be used within <Tabs>.');
11
+
12
+ return value;
13
+ }
@@ -0,0 +1,55 @@
1
+ import React from 'react';
2
+
3
+ import { useControllableState } from '../../../internal/useControllableState';
4
+ import type { TabRegistration, TabsContextValue, TabsProps } from '../../../types/tabs';
5
+ import { resolveTabElementId } from '../utils/resolveTabElementId';
6
+
7
+ /*** Owns controlled state, registration, focus state, and ids for one Tabs instance. */
8
+ export function useTabsController({
9
+ defaultValue,
10
+ onValueChange,
11
+ testID,
12
+ value,
13
+ }: Pick<TabsProps, 'defaultValue' | 'onValueChange' | 'testID' | 'value'>): TabsContextValue {
14
+ const [activeValue, setActiveValue] = useControllableState<string | undefined>({
15
+ defaultValue,
16
+ onChange: (nextValue) => {
17
+ if (nextValue !== undefined) onValueChange?.(nextValue);
18
+ },
19
+ value,
20
+ });
21
+ const [focusedValue, setFocusedValue] = React.useState<string | undefined>(undefined);
22
+ const [tabs, setTabs] = React.useState<readonly TabRegistration[]>([]);
23
+
24
+ const registerTab = React.useCallback((tab: TabRegistration) => {
25
+ setTabs((current) =>
26
+ current.some((entry) => entry.value === tab.value)
27
+ ? current.map((entry) => (entry.value === tab.value ? tab : entry))
28
+ : [...current, tab],
29
+ );
30
+ }, []);
31
+ const unregisterTab = React.useCallback((valueToRemove: string) => {
32
+ setTabs((current) => current.filter((entry) => entry.value !== valueToRemove));
33
+ }, []);
34
+
35
+ React.useEffect(() => {
36
+ if (activeValue !== undefined) return;
37
+ const firstEnabledTab = tabs.find((entry) => !entry.disabled);
38
+ if (firstEnabledTab) setActiveValue(firstEnabledTab.value);
39
+ }, [activeValue, setActiveValue, tabs]);
40
+
41
+ return React.useMemo(
42
+ () => ({
43
+ activeValue,
44
+ focusedValue,
45
+ getPanelId: (tabValue: string) => resolveTabElementId('panel', testID, tabValue),
46
+ getTabId: (tabValue: string) => resolveTabElementId('tab', testID, tabValue),
47
+ registerTab,
48
+ setActiveValue: (nextValue: string) => setActiveValue(nextValue),
49
+ setFocusedValue,
50
+ tabs,
51
+ unregisterTab,
52
+ }),
53
+ [activeValue, focusedValue, registerTab, setActiveValue, tabs, testID, unregisterTab],
54
+ );
55
+ }
@@ -0,0 +1,5 @@
1
+ export type { TabListProps, TabPanelProps, TabProps, TabsProps } from '../../types/tabs';
2
+ export { Tab } from './adapters/inbound/Tab';
3
+ export { TabList } from './adapters/inbound/TabList';
4
+ export { TabPanel } from './adapters/inbound/TabPanel';
5
+ export { Tabs } from './adapters/inbound/Tabs';
@@ -1,12 +1,12 @@
1
1
  import { describe, expect, it } from 'bun:test';
2
2
 
3
- import { resolveNextTabValue } from './navigation';
3
+ import { resolveNextTabValue } from './resolveNextTabValue';
4
4
 
5
5
  const tabs = [
6
6
  { disabled: false, focus: () => undefined, value: 'overview' },
7
7
  { disabled: true, focus: () => undefined, value: 'disabled' },
8
8
  { disabled: false, focus: () => undefined, value: 'details' },
9
- ];
9
+ ] as const;
10
10
 
11
11
  describe('resolveNextTabValue', () => {
12
12
  it('skips disabled tabs when cycling', () => {
@@ -0,0 +1,24 @@
1
+ import type { TabNavigationKey, TabRegistration } from '../../../types/tabs';
2
+
3
+ /*** Resolves the next enabled tab for keyboard focus navigation. */
4
+ export function resolveNextTabValue(
5
+ tabs: readonly TabRegistration[],
6
+ currentValue: string | undefined,
7
+ key: TabNavigationKey,
8
+ ): string | undefined {
9
+ const enabledTabs = tabs.filter((tab) => !tab.disabled);
10
+ if (enabledTabs.length === 0) return undefined;
11
+
12
+ if (key === 'Home') return enabledTabs.at(0)?.value;
13
+ if (key === 'End') return enabledTabs.at(-1)?.value;
14
+
15
+ const currentIndex = enabledTabs.findIndex((tab) => tab.value === currentValue);
16
+ if (currentIndex === -1) return enabledTabs.at(0)?.value;
17
+
18
+ const nextIndex =
19
+ key === 'ArrowLeft' || key === 'ArrowUp'
20
+ ? (currentIndex - 1 + enabledTabs.length) % enabledTabs.length
21
+ : (currentIndex + 1) % enabledTabs.length;
22
+
23
+ return enabledTabs.at(nextIndex)?.value;
24
+ }
@@ -0,0 +1,19 @@
1
+ import { describe, expect, it } from 'bun:test';
2
+
3
+ import { resolveTabElementId } from './resolveTabElementId';
4
+
5
+ describe('resolveTabElementId', () => {
6
+ it('creates stable ids from tab values', () => {
7
+ expect(resolveTabElementId('tab', undefined, 'Account Settings')).toBe(
8
+ 'tabs-tab-Account-Settings',
9
+ );
10
+ expect(resolveTabElementId('panel', undefined, 'Account Settings')).toBe(
11
+ 'tabs-panel-Account-Settings',
12
+ );
13
+ });
14
+
15
+ it('namespaces ids by tabs testID when provided', () => {
16
+ expect(resolveTabElementId('tab', 'settings', 'billing')).toBe('settings-tabs-tab-billing');
17
+ expect(resolveTabElementId('panel', 'settings', 'billing')).toBe('settings-tabs-panel-billing');
18
+ });
19
+ });
@@ -0,0 +1,14 @@
1
+ /*** Creates a stable tab or tab-panel id from a Tabs test id and tab value. */
2
+ export function resolveTabElementId(
3
+ kind: 'tab' | 'panel',
4
+ testID: string | undefined,
5
+ value: string,
6
+ ): string {
7
+ const base = testID ? `${testID}-tabs` : 'tabs';
8
+ return `${base}-${kind}-${sanitizeTabValue(value)}`;
9
+ }
10
+
11
+ /*** Normalizes a tab value for use inside an accessibility id. */
12
+ function sanitizeTabValue(value: string): string {
13
+ return value.trim().replace(/[^a-zA-Z0-9_-]+/g, '-');
14
+ }
@@ -0,0 +1,56 @@
1
+ import React from 'react';
2
+ import { Platform, Pressable, View } from 'react-native';
3
+
4
+ import { useTheme } from '../../../../theme/ThemeContext';
5
+ import type { TooltipProps } from '../../../../types/tooltip';
6
+ import { Popover } from '../../../popover/public';
7
+ import { Surface } from '../../../surface/public';
8
+ import { Text } from '../../../typography/public';
9
+ import { useTooltipVisibility } from '../../composition/useTooltipVisibility';
10
+
11
+ /*** Presents delayed hover/focus help through the shared Popover foundation. */
12
+ export function Tooltip({
13
+ children,
14
+ content,
15
+ delay = 150,
16
+ interactionPolicy = 'enabled',
17
+ placement = 'top',
18
+ testID,
19
+ }: TooltipProps) {
20
+ const { theme } = useTheme();
21
+ const passive = interactionPolicy === 'passive';
22
+ const { hide, setVisible, show, visible } = useTooltipVisibility(delay, passive);
23
+
24
+ return (
25
+ <Popover
26
+ closeOnOutsidePress={false}
27
+ interactionPolicy={interactionPolicy}
28
+ onOpenChange={setVisible}
29
+ open={visible}
30
+ placement={placement}
31
+ testID={testID}
32
+ anchor={() => (
33
+ <Pressable
34
+ onBlur={passive ? undefined : hide}
35
+ onFocus={passive ? undefined : show}
36
+ onHoverIn={Platform.OS === 'web' && !passive ? show : undefined}
37
+ onHoverOut={Platform.OS === 'web' && !passive ? hide : undefined}
38
+ >
39
+ {children}
40
+ </Pressable>
41
+ )}
42
+ >
43
+ <View pointerEvents="none">
44
+ <Surface
45
+ p="s"
46
+ style={{ backgroundColor: theme.semantics.surface.inverse }}
47
+ variant="raised"
48
+ >
49
+ <Text emphasis="inverse" variant="caption">
50
+ {content}
51
+ </Text>
52
+ </Surface>
53
+ </View>
54
+ </Popover>
55
+ );
56
+ }
@@ -0,0 +1,29 @@
1
+ import React from 'react';
2
+
3
+ /*** Manages delayed tooltip visibility and timer cleanup. */
4
+ export function useTooltipVisibility(delay: number, passive: boolean) {
5
+ const timeoutRef = React.useRef<ReturnType<typeof setTimeout> | null>(null);
6
+ const [visible, setVisible] = React.useState(false);
7
+
8
+ const clearTimer = React.useCallback(() => {
9
+ if (timeoutRef.current) {
10
+ clearTimeout(timeoutRef.current);
11
+ timeoutRef.current = null;
12
+ }
13
+ }, []);
14
+
15
+ const show = React.useCallback(() => {
16
+ if (passive) return;
17
+ clearTimer();
18
+ timeoutRef.current = setTimeout(() => setVisible(true), delay);
19
+ }, [clearTimer, delay, passive]);
20
+
21
+ const hide = React.useCallback(() => {
22
+ clearTimer();
23
+ setVisible(false);
24
+ }, [clearTimer]);
25
+
26
+ React.useEffect(() => clearTimer, [clearTimer]);
27
+
28
+ return { hide, setVisible, show, visible } as const;
29
+ }
@@ -0,0 +1,11 @@
1
+ import { expect, test } from 'bun:test';
2
+
3
+ test('Tooltip delegates anchored positioning and portal ownership to Popover', async () => {
4
+ const source = await Bun.file('src/features/tooltip/adapters/inbound/Tooltip.tsx').text();
5
+
6
+ expect(source).toContain("import { Popover } from '../../../popover/public';");
7
+ expect(source).toContain('<Popover');
8
+ expect(source).not.toContain('measureInWindow');
9
+ expect(source).not.toContain('internal/overlay');
10
+ expect(source).not.toContain('<Portal');
11
+ });
@@ -0,0 +1,2 @@
1
+ export type { TooltipProps } from '../../types/tooltip';
2
+ export { Tooltip } from './adapters/inbound/Tooltip';
package/src/index.test.ts CHANGED
@@ -28,8 +28,12 @@ const expectedFeatureRootExports = [
28
28
  "export { Image } from './features/image/public';",
29
29
  "export { KeyboardAvoidingView } from './features/keyboard-avoiding-view/public';",
30
30
  "export { Box, Container, Divider, Grid, Stack } from './features/layout/public';",
31
+ "export { Modal } from './features/modal/public';",
32
+ "export { PopoverMenu } from './features/popover-menu/public';",
31
33
  "export { Popover } from './features/popover/public';",
32
34
  "export { Surface } from './features/surface/public';",
35
+ "export { Tab, TabList, TabPanel, Tabs } from './features/tabs/public';",
36
+ "export { Tooltip } from './features/tooltip/public';",
33
37
  "export { Toast, ToastProvider, useToast } from './features/toast/public';",
34
38
  "export { Heading, Text } from './features/typography/public';",
35
39
  ] as const;
@@ -39,12 +43,8 @@ const expectedLegacyRootExports = [
39
43
  "export { HelperText } from './components/helper-text';",
40
44
  "export { Label } from './components/label';",
41
45
  "export { ListItem } from './components/list-item';",
42
- "export { Menu } from './components/menu';",
43
- "export { Modal } from './components/modal';",
44
46
  "export { Switch } from './components/switch';",
45
- "export { Tab, TabList, TabPanel, Tabs } from './components/tabs';",
46
47
  "export { Textarea } from './components/textarea';",
47
- "export { Tooltip } from './components/tooltip';",
48
48
  "export type { InteractionPolicy, InteractionPolicyProps } from './interactionPolicy';",
49
49
  "export * from './core/responsive';",
50
50
  "export * from './layout';",
@@ -62,9 +62,13 @@ describe('feature-owned root barrel contract', () => {
62
62
  './components/card',
63
63
  './components/checkbox',
64
64
  './components/icon-button',
65
+ './components/menu',
66
+ './components/modal',
65
67
  './components/radio',
68
+ './components/tabs',
66
69
  './components/text-input',
67
70
  './components/toast',
71
+ './components/tooltip',
68
72
  './primitives/heading',
69
73
  './primitives/icon',
70
74
  './primitives/image',
@@ -94,12 +98,15 @@ describe('remaining root barrel contract', () => {
94
98
  expectedLegacyRootExports.forEach((line) => expect(indexSource).toContain(line));
95
99
  });
96
100
 
97
- it('does not retain obsolete action-sheet, drawer, or navigation chrome', () => {
101
+ it('does not retain obsolete action-sheet, drawer, menu aliases, or navigation chrome', () => {
98
102
  expect(indexSource).not.toContain('ActionSheet');
99
103
  expect(indexSource).not.toContain("'./components/action-sheet'");
100
104
  expect(indexSource).not.toContain("'./components/drawer'");
101
105
  expect(indexSource).not.toContain("'./components/navigation'");
102
106
  expect(indexSource).not.toContain('DrawerNavigation');
107
+ expect(indexSource).not.toMatch(/\bMenuProps\b/u);
108
+ expect(indexSource).not.toMatch(/\bMenuAction\b/u);
109
+ expect(indexSource).not.toMatch(/export\s*\{\s*Menu\s*\}/u);
103
110
  expect(indexSource).not.toContain('NavigationItem');
104
111
  expect(indexSource).not.toContain('NavigationList');
105
112
  expect(indexSource).not.toContain('TabBar');
package/src/index.ts CHANGED
@@ -6,18 +6,10 @@ export type { LabelProps } from './components/label';
6
6
  export { Label } from './components/label';
7
7
  export type { ListItemProps } from './components/list-item';
8
8
  export { ListItem } from './components/list-item';
9
- export type { MenuAction, MenuActionIntent, MenuItem, MenuProps } from './components/menu';
10
- export { Menu } from './components/menu';
11
- export type { ModalProps } from './components/modal';
12
- export { Modal } from './components/modal';
13
9
  export type { SwitchProps } from './components/switch';
14
10
  export { Switch } from './components/switch';
15
- export type { TabListProps, TabPanelProps, TabProps, TabsProps } from './components/tabs';
16
- export { Tab, TabList, TabPanel, Tabs } from './components/tabs';
17
11
  export type { TextareaProps } from './components/textarea';
18
12
  export { Textarea } from './components/textarea';
19
- export type { TooltipProps } from './components/tooltip';
20
- export { Tooltip } from './components/tooltip';
21
13
  export type { FontRuntime } from './context/FontContext';
22
14
  export { FontProvider, useFontContext } from './context/FontContext';
23
15
  export type { I18nInstance, TranslationRuntime, Translator } from './context/TranslationContext';
@@ -74,14 +66,24 @@ export type {
74
66
  StackProps,
75
67
  } from './features/layout/public';
76
68
  export { Box, Container, Divider, Grid, Stack } from './features/layout/public';
69
+ export type { ModalProps } from './features/modal/public';
70
+ export { Modal } from './features/modal/public';
77
71
  export type {
78
72
  PopoverAnchorRenderProps,
79
73
  PopoverPlacement,
80
74
  PopoverProps,
81
75
  } from './features/popover/public';
82
76
  export { Popover } from './features/popover/public';
77
+ export type {
78
+ PopoverMenuAction,
79
+ PopoverMenuActionIntent,
80
+ PopoverMenuProps,
81
+ } from './features/popover-menu/public';
82
+ export { PopoverMenu } from './features/popover-menu/public';
83
83
  export type { SurfaceProps, SurfaceVariant } from './features/surface/public';
84
84
  export { Surface } from './features/surface/public';
85
+ export type { TabListProps, TabPanelProps, TabProps, TabsProps } from './features/tabs/public';
86
+ export { Tab, TabList, TabPanel, Tabs } from './features/tabs/public';
85
87
  export type {
86
88
  ToastController,
87
89
  ToastOptions,
@@ -90,6 +92,8 @@ export type {
90
92
  ToastStatus,
91
93
  } from './features/toast/public';
92
94
  export { Toast, ToastProvider, useToast } from './features/toast/public';
95
+ export type { TooltipProps } from './features/tooltip/public';
96
+ export { Tooltip } from './features/tooltip/public';
93
97
  export type { HeadingLevel, HeadingProps, TextProps } from './features/typography/public';
94
98
  export { Heading, Text } from './features/typography/public';
95
99
  export type { InteractionPolicy, InteractionPolicyProps } from './interactionPolicy';
@@ -1,10 +1,14 @@
1
1
  import type React from 'react';
2
2
 
3
+ export type BottomSheetContentMode = 'view' | 'direct';
4
+
3
5
  export interface BottomSheetPresentOptions {
4
6
  content: React.ReactNode;
7
+ contentMode?: BottomSheetContentMode;
5
8
  snapPoints?: readonly (number | string)[];
6
9
  initialIndex?: number;
7
10
  enableDynamicSizing?: boolean;
11
+ maxDynamicContentSize?: number;
8
12
  enablePanDownToClose?: boolean;
9
13
  dismissOnBackdropPress?: boolean;
10
14
  keyboardBehavior?: 'extend' | 'fillParent' | 'interactive';
@@ -1,6 +1,6 @@
1
1
  import type React from 'react';
2
2
 
3
- import type { InteractionPolicy } from '../../interactionPolicy';
3
+ import type { InteractionPolicy } from '../interactionPolicy';
4
4
 
5
5
  export interface ModalProps {
6
6
  visible: boolean;
@@ -0,0 +1,27 @@
1
+ import type React from 'react';
2
+
3
+ import type { InteractionPolicy } from '../interactionPolicy';
4
+ import type { PopoverAnchorRenderProps } from './popover';
5
+
6
+ export type PopoverMenuActionIntent = 'default' | 'danger';
7
+
8
+ export interface PopoverMenuAction {
9
+ id: string;
10
+ title: React.ReactNode;
11
+ description?: React.ReactNode;
12
+ leading?: React.ReactNode;
13
+ trailing?: React.ReactNode;
14
+ intent?: PopoverMenuActionIntent;
15
+ disabled?: boolean;
16
+ selected?: boolean;
17
+ activate?: () => void;
18
+ }
19
+
20
+ export interface PopoverMenuProps {
21
+ trigger: (controls: PopoverAnchorRenderProps) => React.ReactNode;
22
+ actions: readonly PopoverMenuAction[];
23
+ dismiss?: () => void;
24
+ closeOnSelect?: boolean;
25
+ interactionPolicy?: InteractionPolicy;
26
+ testID?: string;
27
+ }