@ankhorage/surface 0.1.5 → 0.1.7

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 (292) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/dist/components/badge/Badge.js.map +1 -1
  3. package/dist/components/badge/index.js.map +1 -1
  4. package/dist/components/badge/types.js.map +1 -1
  5. package/dist/components/button/Button.js.map +1 -1
  6. package/dist/components/button/index.js.map +1 -1
  7. package/dist/components/button/types.js.map +1 -1
  8. package/dist/components/card/Card.js.map +1 -1
  9. package/dist/components/card/index.js.map +1 -1
  10. package/dist/components/card/types.js.map +1 -1
  11. package/dist/components/checkbox/Checkbox.js.map +1 -1
  12. package/dist/components/checkbox/index.js.map +1 -1
  13. package/dist/components/checkbox/types.js.map +1 -1
  14. package/dist/components/drawer/Drawer.js.map +1 -1
  15. package/dist/components/drawer/index.js.map +1 -1
  16. package/dist/components/drawer/types.js.map +1 -1
  17. package/dist/components/field/Field.js.map +1 -1
  18. package/dist/components/field/index.js.map +1 -1
  19. package/dist/components/field/types.js.map +1 -1
  20. package/dist/components/helper-text/HelperText.js.map +1 -1
  21. package/dist/components/helper-text/index.js.map +1 -1
  22. package/dist/components/helper-text/types.js.map +1 -1
  23. package/dist/components/icon-button/IconButton.js.map +1 -1
  24. package/dist/components/icon-button/index.js.map +1 -1
  25. package/dist/components/icon-button/types.js.map +1 -1
  26. package/dist/components/label/Label.js.map +1 -1
  27. package/dist/components/label/index.js.map +1 -1
  28. package/dist/components/label/types.js.map +1 -1
  29. package/dist/components/list-item/ListItem.js.map +1 -1
  30. package/dist/components/list-item/index.js.map +1 -1
  31. package/dist/components/list-item/types.js.map +1 -1
  32. package/dist/components/menu/Menu.js.map +1 -1
  33. package/dist/components/menu/index.js.map +1 -1
  34. package/dist/components/menu/navigation.js.map +1 -1
  35. package/dist/components/menu/types.js.map +1 -1
  36. package/dist/components/modal/Modal.js.map +1 -1
  37. package/dist/components/modal/index.js.map +1 -1
  38. package/dist/components/modal/types.js.map +1 -1
  39. package/dist/components/radio/Radio.js.map +1 -1
  40. package/dist/components/radio/index.js.map +1 -1
  41. package/dist/components/radio/types.js.map +1 -1
  42. package/dist/components/switch/Switch.js.map +1 -1
  43. package/dist/components/switch/index.js.map +1 -1
  44. package/dist/components/switch/types.js.map +1 -1
  45. package/dist/components/tabs/Tab.js.map +1 -1
  46. package/dist/components/tabs/TabList.js.map +1 -1
  47. package/dist/components/tabs/TabPanel.js.map +1 -1
  48. package/dist/components/tabs/Tabs.js.map +1 -1
  49. package/dist/components/tabs/a11y.js.map +1 -1
  50. package/dist/components/tabs/context.js.map +1 -1
  51. package/dist/components/tabs/index.js.map +1 -1
  52. package/dist/components/tabs/navigation.js.map +1 -1
  53. package/dist/components/tabs/types.js.map +1 -1
  54. package/dist/components/text-input/TextInput.js.map +1 -1
  55. package/dist/components/text-input/index.js.map +1 -1
  56. package/dist/components/text-input/types.js.map +1 -1
  57. package/dist/components/textarea/Textarea.js.map +1 -1
  58. package/dist/components/textarea/index.js.map +1 -1
  59. package/dist/components/textarea/types.js.map +1 -1
  60. package/dist/components/toast/Toast.js.map +1 -1
  61. package/dist/components/toast/ToastProvider.js.map +1 -1
  62. package/dist/components/toast/index.js.map +1 -1
  63. package/dist/components/toast/types.js.map +1 -1
  64. package/dist/components/tooltip/Tooltip.js.map +1 -1
  65. package/dist/components/tooltip/index.js.map +1 -1
  66. package/dist/components/tooltip/types.js.map +1 -1
  67. package/dist/context/FontContext.js.map +1 -1
  68. package/dist/context/TranslationContext.js.map +1 -1
  69. package/dist/core/responsive/ResponsiveProvider.js.map +1 -1
  70. package/dist/core/responsive/breakpoints.js.map +1 -1
  71. package/dist/core/responsive/getBreakpointFromWidth.js.map +1 -1
  72. package/dist/core/responsive/index.js.map +1 -1
  73. package/dist/core/responsive/resolve.js.map +1 -1
  74. package/dist/core/responsive/types.js.map +1 -1
  75. package/dist/core/responsive/useBreakpoint.js.map +1 -1
  76. package/dist/examples/DocsExamples.js.map +1 -1
  77. package/dist/index.js.map +1 -1
  78. package/dist/internal/focus/FocusScope.js.map +1 -1
  79. package/dist/internal/focus/useFocusManager.js.map +1 -1
  80. package/dist/internal/overlay/OverlayProvider.js.map +1 -1
  81. package/dist/internal/overlay/Portal.js.map +1 -1
  82. package/dist/internal/overlay/useOverlayStack.js.map +1 -1
  83. package/dist/internal/resolvers/index.js.map +1 -1
  84. package/dist/internal/resolvers/resolveControlSize.js.map +1 -1
  85. package/dist/internal/resolvers/resolveFieldPresentation.js.map +1 -1
  86. package/dist/internal/resolvers/resolveFieldState.js.map +1 -1
  87. package/dist/internal/resolvers/resolveFocusRingStyles.js.map +1 -1
  88. package/dist/internal/resolvers/resolveIconSize.js.map +1 -1
  89. package/dist/internal/resolvers/resolveIndicatorSize.js.map +1 -1
  90. package/dist/internal/resolvers/resolveInteractiveColors.js.map +1 -1
  91. package/dist/internal/resolvers/resolveInteractiveState.js.map +1 -1
  92. package/dist/internal/resolvers/resolveOverlayAnimation.js.map +1 -1
  93. package/dist/internal/resolvers/resolveOverlayZIndex.js.map +1 -1
  94. package/dist/internal/resolvers/resolveSelectionControlBehavior.js.map +1 -1
  95. package/dist/internal/resolvers/resolveSelectionControlColors.js.map +1 -1
  96. package/dist/internal/resolvers/resolveTextColor.js.map +1 -1
  97. package/dist/internal/resolvers/resolveTextStyles.js.map +1 -1
  98. package/dist/internal/resolvers/resolveTone.js.map +1 -1
  99. package/dist/internal/useControllableState.js.map +1 -1
  100. package/dist/layout/Box.js.map +1 -1
  101. package/dist/layout/Center.js.map +1 -1
  102. package/dist/layout/Container.js.map +1 -1
  103. package/dist/layout/Divider.js.map +1 -1
  104. package/dist/layout/Grid.js.map +1 -1
  105. package/dist/layout/Inline.js.map +1 -1
  106. package/dist/layout/Show.js.map +1 -1
  107. package/dist/layout/Spacer.js.map +1 -1
  108. package/dist/layout/Stack.js.map +1 -1
  109. package/dist/layout/Surface.js.map +1 -1
  110. package/dist/layout/Template.js.map +1 -1
  111. package/dist/layout/helpers.js.map +1 -1
  112. package/dist/layout/index.js.map +1 -1
  113. package/dist/primitives/button-base/ButtonBase.js.map +1 -1
  114. package/dist/primitives/button-base/index.js.map +1 -1
  115. package/dist/primitives/button-base/types.js.map +1 -1
  116. package/dist/primitives/heading/Heading.js.map +1 -1
  117. package/dist/primitives/heading/index.js.map +1 -1
  118. package/dist/primitives/heading/resolveHeadingStyle.js.map +1 -1
  119. package/dist/primitives/heading/types.js.map +1 -1
  120. package/dist/primitives/icon/Icon.js.map +1 -1
  121. package/dist/primitives/icon/index.js.map +1 -1
  122. package/dist/primitives/icon/resolveExpoIconComponent.js.map +1 -1
  123. package/dist/primitives/text/Text.js.map +1 -1
  124. package/dist/primitives/text/index.js.map +1 -1
  125. package/dist/primitives/text/types.js.map +1 -1
  126. package/dist/theme/ThemeContext.js.map +1 -1
  127. package/dist/theme/colorEngine.js +1 -1
  128. package/dist/theme/colorEngine.js.map +1 -1
  129. package/dist/theme/createTheme.js.map +1 -1
  130. package/dist/theme/index.js.map +1 -1
  131. package/dist/theme/resolveToken.js.map +1 -1
  132. package/dist/theme/types.js.map +1 -1
  133. package/dist/utils/deepEqual.js.map +1 -1
  134. package/dist/utils/deepMerge.js.map +1 -1
  135. package/package.json +4 -1
  136. package/src/components/badge/Badge.tsx +47 -0
  137. package/src/components/badge/index.ts +2 -0
  138. package/src/components/badge/types.ts +13 -0
  139. package/src/components/button/Button.tsx +104 -0
  140. package/src/components/button/index.ts +2 -0
  141. package/src/components/button/types.ts +26 -0
  142. package/src/components/card/Card.tsx +81 -0
  143. package/src/components/card/index.ts +2 -0
  144. package/src/components/card/types.ts +11 -0
  145. package/src/components/checkbox/Checkbox.tsx +111 -0
  146. package/src/components/checkbox/index.ts +2 -0
  147. package/src/components/checkbox/types.ts +19 -0
  148. package/src/components/drawer/Drawer.tsx +92 -0
  149. package/src/components/drawer/index.ts +2 -0
  150. package/src/components/drawer/types.ts +10 -0
  151. package/src/components/field/Field.tsx +43 -0
  152. package/src/components/field/index.ts +2 -0
  153. package/src/components/field/types.ts +13 -0
  154. package/src/components/helper-text/HelperText.tsx +12 -0
  155. package/src/components/helper-text/index.ts +2 -0
  156. package/src/components/helper-text/types.ts +9 -0
  157. package/src/components/icon-button/IconButton.tsx +60 -0
  158. package/src/components/icon-button/index.ts +2 -0
  159. package/src/components/icon-button/types.ts +19 -0
  160. package/src/components/label/Label.tsx +17 -0
  161. package/src/components/label/index.ts +2 -0
  162. package/src/components/label/types.ts +10 -0
  163. package/src/components/list-item/ListItem.tsx +72 -0
  164. package/src/components/list-item/index.ts +2 -0
  165. package/src/components/list-item/types.ts +11 -0
  166. package/src/components/menu/Menu.tsx +180 -0
  167. package/src/components/menu/index.ts +2 -0
  168. package/src/components/menu/navigation.test.ts +21 -0
  169. package/src/components/menu/navigation.ts +34 -0
  170. package/src/components/menu/types.ts +16 -0
  171. package/src/components/modal/Modal.tsx +87 -0
  172. package/src/components/modal/index.ts +2 -0
  173. package/src/components/modal/types.ts +9 -0
  174. package/src/components/radio/Radio.tsx +116 -0
  175. package/src/components/radio/index.ts +2 -0
  176. package/src/components/radio/types.ts +19 -0
  177. package/src/components/switch/Switch.tsx +116 -0
  178. package/src/components/switch/index.ts +2 -0
  179. package/src/components/switch/types.ts +19 -0
  180. package/src/components/tabs/Tab.tsx +82 -0
  181. package/src/components/tabs/TabList.tsx +51 -0
  182. package/src/components/tabs/TabPanel.tsx +29 -0
  183. package/src/components/tabs/Tabs.tsx +67 -0
  184. package/src/components/tabs/a11y.test.ts +15 -0
  185. package/src/components/tabs/a11y.ts +15 -0
  186. package/src/components/tabs/context.tsx +31 -0
  187. package/src/components/tabs/index.ts +5 -0
  188. package/src/components/tabs/navigation.test.ts +21 -0
  189. package/src/components/tabs/navigation.ts +32 -0
  190. package/src/components/tabs/types.ts +27 -0
  191. package/src/components/text-input/TextInput.tsx +116 -0
  192. package/src/components/text-input/index.ts +2 -0
  193. package/src/components/text-input/types.ts +32 -0
  194. package/src/components/textarea/Textarea.tsx +15 -0
  195. package/src/components/textarea/index.ts +2 -0
  196. package/src/components/textarea/types.ts +5 -0
  197. package/src/components/toast/Toast.tsx +54 -0
  198. package/src/components/toast/ToastProvider.tsx +114 -0
  199. package/src/components/toast/index.ts +3 -0
  200. package/src/components/toast/types.ts +16 -0
  201. package/src/components/tooltip/Tooltip.tsx +109 -0
  202. package/src/components/tooltip/index.ts +2 -0
  203. package/src/components/tooltip/types.ts +9 -0
  204. package/src/context/FontContext.tsx +59 -0
  205. package/src/context/TranslationContext.tsx +54 -0
  206. package/src/core/responsive/ResponsiveProvider.tsx +31 -0
  207. package/src/core/responsive/breakpoints.ts +9 -0
  208. package/src/core/responsive/getBreakpointFromWidth.test.ts +15 -0
  209. package/src/core/responsive/getBreakpointFromWidth.ts +10 -0
  210. package/src/core/responsive/index.ts +6 -0
  211. package/src/core/responsive/resolve.test.ts +25 -0
  212. package/src/core/responsive/resolve.ts +24 -0
  213. package/src/core/responsive/types.ts +10 -0
  214. package/src/core/responsive/useBreakpoint.ts +9 -0
  215. package/src/examples/DocsExamples.tsx +116 -0
  216. package/src/index.test.ts +64 -0
  217. package/src/index.ts +55 -0
  218. package/src/internal/focus/FocusScope.tsx +66 -0
  219. package/src/internal/focus/useFocusManager.test.ts +44 -0
  220. package/src/internal/focus/useFocusManager.ts +142 -0
  221. package/src/internal/overlay/OverlayProvider.tsx +74 -0
  222. package/src/internal/overlay/Portal.tsx +38 -0
  223. package/src/internal/overlay/useOverlayStack.test.ts +31 -0
  224. package/src/internal/overlay/useOverlayStack.ts +61 -0
  225. package/src/internal/resolvers/index.ts +15 -0
  226. package/src/internal/resolvers/resolveControlSize.test.ts +25 -0
  227. package/src/internal/resolvers/resolveControlSize.ts +45 -0
  228. package/src/internal/resolvers/resolveFieldPresentation.test.ts +31 -0
  229. package/src/internal/resolvers/resolveFieldPresentation.ts +30 -0
  230. package/src/internal/resolvers/resolveFieldState.test.ts +22 -0
  231. package/src/internal/resolvers/resolveFieldState.ts +36 -0
  232. package/src/internal/resolvers/resolveFocusRingStyles.ts +14 -0
  233. package/src/internal/resolvers/resolveIconSize.ts +6 -0
  234. package/src/internal/resolvers/resolveIndicatorSize.test.ts +19 -0
  235. package/src/internal/resolvers/resolveIndicatorSize.ts +47 -0
  236. package/src/internal/resolvers/resolveInteractiveColors.test.ts +57 -0
  237. package/src/internal/resolvers/resolveInteractiveColors.ts +134 -0
  238. package/src/internal/resolvers/resolveInteractiveState.test.ts +14 -0
  239. package/src/internal/resolvers/resolveInteractiveState.ts +15 -0
  240. package/src/internal/resolvers/resolveOverlayAnimation.test.ts +15 -0
  241. package/src/internal/resolvers/resolveOverlayAnimation.ts +24 -0
  242. package/src/internal/resolvers/resolveOverlayZIndex.test.ts +15 -0
  243. package/src/internal/resolvers/resolveOverlayZIndex.ts +13 -0
  244. package/src/internal/resolvers/resolveSelectionControlBehavior.test.ts +52 -0
  245. package/src/internal/resolvers/resolveSelectionControlBehavior.ts +23 -0
  246. package/src/internal/resolvers/resolveSelectionControlColors.test.ts +44 -0
  247. package/src/internal/resolvers/resolveSelectionControlColors.ts +81 -0
  248. package/src/internal/resolvers/resolveTextColor.test.ts +23 -0
  249. package/src/internal/resolvers/resolveTextColor.ts +40 -0
  250. package/src/internal/resolvers/resolveTextStyles.test.ts +27 -0
  251. package/src/internal/resolvers/resolveTextStyles.ts +95 -0
  252. package/src/internal/resolvers/resolveTone.ts +19 -0
  253. package/src/internal/useControllableState.ts +28 -0
  254. package/src/layout/Box.tsx +79 -0
  255. package/src/layout/Center.tsx +22 -0
  256. package/src/layout/Container.tsx +43 -0
  257. package/src/layout/Divider.tsx +26 -0
  258. package/src/layout/Grid.tsx +83 -0
  259. package/src/layout/Inline.tsx +9 -0
  260. package/src/layout/Show.tsx +15 -0
  261. package/src/layout/Spacer.tsx +22 -0
  262. package/src/layout/Stack.tsx +67 -0
  263. package/src/layout/Surface.tsx +70 -0
  264. package/src/layout/Template.tsx +85 -0
  265. package/src/layout/helpers.test.ts +71 -0
  266. package/src/layout/helpers.ts +208 -0
  267. package/src/layout/index.ts +22 -0
  268. package/src/primitives/button-base/ButtonBase.tsx +81 -0
  269. package/src/primitives/button-base/index.ts +2 -0
  270. package/src/primitives/button-base/types.ts +16 -0
  271. package/src/primitives/heading/Heading.tsx +60 -0
  272. package/src/primitives/heading/index.ts +2 -0
  273. package/src/primitives/heading/resolveHeadingStyle.test.ts +31 -0
  274. package/src/primitives/heading/resolveHeadingStyle.ts +17 -0
  275. package/src/primitives/heading/types.ts +13 -0
  276. package/src/primitives/icon/Icon.tsx +40 -0
  277. package/src/primitives/icon/index.ts +2 -0
  278. package/src/primitives/icon/resolveExpoIconComponent.test.ts +29 -0
  279. package/src/primitives/icon/resolveExpoIconComponent.ts +20 -0
  280. package/src/primitives/text/Text.tsx +66 -0
  281. package/src/primitives/text/index.ts +2 -0
  282. package/src/primitives/text/types.ts +18 -0
  283. package/src/theme/ThemeContext.tsx +95 -0
  284. package/src/theme/colorEngine.test.ts +114 -0
  285. package/src/theme/colorEngine.ts +480 -0
  286. package/src/theme/createTheme.ts +121 -0
  287. package/src/theme/index.ts +5 -0
  288. package/src/theme/resolveToken.ts +32 -0
  289. package/src/theme/types.ts +188 -0
  290. package/src/utils/deepEqual.ts +34 -0
  291. package/src/utils/deepMerge.test.ts +117 -0
  292. package/src/utils/deepMerge.ts +29 -0
@@ -0,0 +1,19 @@
1
+ import type React from 'react';
2
+
3
+ import type { ControlSize } from '../../internal/resolvers/resolveControlSize';
4
+ import type { ComponentTone } from '../../internal/resolvers/resolveTone';
5
+ import type { ButtonBaseProps } from '../../primitives/button-base';
6
+
7
+ export interface RadioProps extends Omit<
8
+ ButtonBaseProps,
9
+ 'accessibilityRole' | 'accessibilityState' | 'children' | 'onPress' | 'style'
10
+ > {
11
+ children?: React.ReactNode;
12
+ checked?: boolean;
13
+ defaultChecked?: boolean;
14
+ onCheckedChange?: ((checked: boolean) => void) | undefined;
15
+ tone?: ComponentTone;
16
+ size?: ControlSize;
17
+ invalid?: boolean;
18
+ readOnly?: boolean;
19
+ }
@@ -0,0 +1,116 @@
1
+ import React from 'react';
2
+
3
+ import {
4
+ resolveFieldState,
5
+ resolveIndicatorSize,
6
+ resolveSelectionControlColors,
7
+ resolveSelectionControlNextChecked,
8
+ } from '../../internal/resolvers';
9
+ import { useControllableState } from '../../internal/useControllableState';
10
+ import { Box } from '../../layout';
11
+ import { ButtonBase } from '../../primitives/button-base';
12
+ import { Text } from '../../primitives/text';
13
+ import { useTheme } from '../../theme/ThemeContext';
14
+ import type { SwitchProps } from './types';
15
+
16
+ export function Switch({
17
+ children,
18
+ checked,
19
+ defaultChecked = false,
20
+ onCheckedChange,
21
+ tone = 'primary',
22
+ size = 'm',
23
+ disabled = false,
24
+ invalid = false,
25
+ readOnly = false,
26
+ accessibilityLabel,
27
+ testID,
28
+ ...props
29
+ }: SwitchProps) {
30
+ const { theme } = useTheme();
31
+ const [isChecked, setChecked] = useControllableState<boolean>({
32
+ value: checked,
33
+ defaultValue: defaultChecked,
34
+ onChange: onCheckedChange,
35
+ });
36
+ const indicatorSize = resolveIndicatorSize(size);
37
+ const nextChecked = resolveSelectionControlNextChecked({
38
+ checked: isChecked,
39
+ disabled,
40
+ kind: 'switch',
41
+ readOnly,
42
+ });
43
+
44
+ return (
45
+ <ButtonBase
46
+ {...props}
47
+ accessibilityLabel={accessibilityLabel}
48
+ accessibilityRole="switch"
49
+ accessibilityState={{ checked: isChecked }}
50
+ disabled={disabled}
51
+ onPress={
52
+ nextChecked === null
53
+ ? undefined
54
+ : () => {
55
+ setChecked(nextChecked);
56
+ }
57
+ }
58
+ testID={testID}
59
+ >
60
+ {(interactionState) => {
61
+ const fieldState = resolveFieldState({
62
+ disabled,
63
+ focused: interactionState.focused,
64
+ invalid,
65
+ readOnly,
66
+ });
67
+ const colors = resolveSelectionControlColors(theme, {
68
+ checked: isChecked,
69
+ fieldState,
70
+ hovered: interactionState.hovered,
71
+ pressed: interactionState.pressed,
72
+ tone,
73
+ });
74
+
75
+ return (
76
+ <Box
77
+ style={{
78
+ alignItems: 'center',
79
+ flexDirection: 'row',
80
+ opacity: colors.opacity,
81
+ }}
82
+ >
83
+ <Box
84
+ radius="full"
85
+ style={{
86
+ backgroundColor: colors.trackColor,
87
+ borderColor: colors.borderColor,
88
+ borderWidth: 1,
89
+ justifyContent: 'center',
90
+ minWidth: indicatorSize.switchWidth,
91
+ paddingHorizontal: 2,
92
+ width: indicatorSize.switchWidth,
93
+ height: indicatorSize.switchHeight,
94
+ }}
95
+ >
96
+ <Box
97
+ radius="full"
98
+ style={{
99
+ alignSelf: isChecked ? 'flex-end' : 'flex-start',
100
+ backgroundColor: colors.thumbColor,
101
+ height: indicatorSize.switchThumb,
102
+ width: indicatorSize.switchThumb,
103
+ }}
104
+ />
105
+ </Box>
106
+ {children ? (
107
+ <Box ml="s">
108
+ <Text color={colors.labelColor}>{children}</Text>
109
+ </Box>
110
+ ) : null}
111
+ </Box>
112
+ );
113
+ }}
114
+ </ButtonBase>
115
+ );
116
+ }
@@ -0,0 +1,2 @@
1
+ export { Switch } from './Switch';
2
+ export type { SwitchProps } from './types';
@@ -0,0 +1,19 @@
1
+ import type React from 'react';
2
+
3
+ import type { ControlSize } from '../../internal/resolvers/resolveControlSize';
4
+ import type { ComponentTone } from '../../internal/resolvers/resolveTone';
5
+ import type { ButtonBaseProps } from '../../primitives/button-base';
6
+
7
+ export interface SwitchProps extends Omit<
8
+ ButtonBaseProps,
9
+ 'accessibilityRole' | 'accessibilityState' | 'children' | 'onPress' | 'style'
10
+ > {
11
+ children?: React.ReactNode;
12
+ checked?: boolean;
13
+ defaultChecked?: boolean;
14
+ onCheckedChange?: ((checked: boolean) => void) | undefined;
15
+ tone?: ComponentTone;
16
+ size?: ControlSize;
17
+ invalid?: boolean;
18
+ readOnly?: boolean;
19
+ }
@@ -0,0 +1,82 @@
1
+ import React from 'react';
2
+ import { Pressable } from 'react-native';
3
+
4
+ import { Box } from '../../layout';
5
+ import { Text } from '../../primitives/text';
6
+ import { useTheme } from '../../theme/ThemeContext';
7
+ import { useTabsContext } from './context';
8
+ import type { TabProps } from './types';
9
+
10
+ export function Tab({ value, children, disabled = false, testID }: TabProps) {
11
+ const { theme } = useTheme();
12
+ const {
13
+ activeValue,
14
+ getPanelId,
15
+ getTabId,
16
+ registerTab,
17
+ setActiveValue,
18
+ setFocusedValue,
19
+ unregisterTab,
20
+ } = useTabsContext();
21
+ const pressableRef = React.useRef<React.ElementRef<typeof Pressable> | null>(null);
22
+ const selected = activeValue === value;
23
+ const tabId = getTabId(value);
24
+ const panelId = getPanelId(value);
25
+
26
+ React.useEffect(() => {
27
+ registerTab({
28
+ disabled,
29
+ focus: () => {
30
+ const focusable = pressableRef.current as unknown as {
31
+ focus?: (() => void) | undefined;
32
+ } | null;
33
+ focusable?.focus?.();
34
+ },
35
+ value,
36
+ });
37
+
38
+ return () => {
39
+ unregisterTab(value);
40
+ };
41
+ }, [disabled, registerTab, unregisterTab, value]);
42
+
43
+ return (
44
+ <Pressable
45
+ accessibilityLabel={undefined}
46
+ accessibilityRole="tab"
47
+ accessibilityState={{ disabled, selected }}
48
+ nativeID={tabId}
49
+ aria-controls={panelId}
50
+ disabled={disabled}
51
+ onFocus={() => setFocusedValue(value)}
52
+ onBlur={() => setFocusedValue(undefined)}
53
+ onPress={() => {
54
+ if (!disabled) {
55
+ setActiveValue(value);
56
+ }
57
+ }}
58
+ ref={pressableRef}
59
+ testID={testID}
60
+ >
61
+ <Box
62
+ px="m"
63
+ py="s"
64
+ style={{
65
+ borderBottomColor: selected
66
+ ? theme.semantics.action.primary.base
67
+ : theme.semantics.border.default,
68
+ borderBottomWidth: 2,
69
+ opacity: disabled ? 0.64 : 1,
70
+ }}
71
+ >
72
+ <Text
73
+ color={selected ? theme.semantics.action.primary.base : theme.semantics.content.default}
74
+ variant="label"
75
+ weight="medium"
76
+ >
77
+ {children}
78
+ </Text>
79
+ </Box>
80
+ </Pressable>
81
+ );
82
+ }
@@ -0,0 +1,51 @@
1
+ import React from 'react';
2
+
3
+ import { useFocusManager } from '../../internal/focus/useFocusManager';
4
+ import { Box } from '../../layout';
5
+ import { useTabsContext } from './context';
6
+ import { resolveNextTabValue } from './navigation';
7
+ import type { TabListProps } from './types';
8
+
9
+ export function TabList({ children, testID }: TabListProps) {
10
+ const { bindKeydown } = useFocusManager();
11
+ const { focusedValue, setActiveValue, tabs } = useTabsContext();
12
+
13
+ React.useEffect(() => {
14
+ if (!focusedValue) {
15
+ return undefined;
16
+ }
17
+
18
+ return bindKeydown((event) => {
19
+ const nextValue =
20
+ event.key === 'ArrowLeft' ||
21
+ event.key === 'ArrowRight' ||
22
+ event.key === 'ArrowUp' ||
23
+ event.key === 'ArrowDown' ||
24
+ event.key === 'Home' ||
25
+ event.key === 'End'
26
+ ? resolveNextTabValue(tabs, focusedValue, event.key)
27
+ : undefined;
28
+
29
+ if (!nextValue) {
30
+ return;
31
+ }
32
+
33
+ event.preventDefault();
34
+ const nextTab = tabs.find((tab) => tab.value === nextValue);
35
+ nextTab?.focus();
36
+ setActiveValue(nextValue);
37
+ });
38
+ }, [bindKeydown, focusedValue, setActiveValue, tabs]);
39
+
40
+ return (
41
+ <Box
42
+ accessibilityRole="tablist"
43
+ style={{
44
+ flexDirection: 'row',
45
+ }}
46
+ testID={testID}
47
+ >
48
+ {children}
49
+ </Box>
50
+ );
51
+ }
@@ -0,0 +1,29 @@
1
+ import React from 'react';
2
+ import { View } from 'react-native';
3
+
4
+ import { useTabsContext } from './context';
5
+ import type { TabPanelProps } from './types';
6
+
7
+ const TAB_PANEL_ROLE = 'tabpanel' as React.ComponentProps<typeof View>['accessibilityRole'];
8
+
9
+ export function TabPanel({ value, children, testID }: TabPanelProps) {
10
+ const { activeValue, getPanelId, getTabId } = useTabsContext();
11
+
12
+ if (activeValue !== value) {
13
+ return null;
14
+ }
15
+
16
+ const tabId = getTabId(value);
17
+ const panelId = getPanelId(value);
18
+
19
+ return (
20
+ <View
21
+ accessibilityLabelledBy={tabId}
22
+ accessibilityRole={TAB_PANEL_ROLE}
23
+ nativeID={panelId}
24
+ testID={testID}
25
+ >
26
+ {children}
27
+ </View>
28
+ );
29
+ }
@@ -0,0 +1,67 @@
1
+ import React from 'react';
2
+
3
+ import { useControllableState } from '../../internal/useControllableState';
4
+ import { Box } from '../../layout';
5
+ import { getTabId as resolveTabId, getTabPanelId as resolveTabPanelId } from './a11y';
6
+ import { type TabRegistration, TabsContext } from './context';
7
+ import type { TabsProps } from './types';
8
+
9
+ export function Tabs({ children, value, defaultValue, onValueChange, testID }: TabsProps) {
10
+ const [activeValue, setActiveValue] = useControllableState<string | undefined>({
11
+ value,
12
+ defaultValue,
13
+ onChange: (nextValue) => {
14
+ if (nextValue !== undefined) {
15
+ onValueChange?.(nextValue);
16
+ }
17
+ },
18
+ });
19
+ const [focusedValue, setFocusedValue] = React.useState<string | undefined>(undefined);
20
+ const [tabs, setTabs] = React.useState<TabRegistration[]>([]);
21
+
22
+ const registerTab = React.useCallback((tab: TabRegistration) => {
23
+ setTabs((current) => {
24
+ return current.some((entry) => entry.value === tab.value)
25
+ ? current.map((entry) => (entry.value === tab.value ? tab : entry))
26
+ : [...current, tab];
27
+ });
28
+ }, []);
29
+
30
+ const unregisterTab = React.useCallback((valueToRemove: string) => {
31
+ setTabs((current) => current.filter((entry) => entry.value !== valueToRemove));
32
+ }, []);
33
+
34
+ React.useEffect(() => {
35
+ if (activeValue !== undefined) {
36
+ return;
37
+ }
38
+
39
+ const firstEnabledTab = tabs.find((entry) => !entry.disabled);
40
+ if (firstEnabledTab) {
41
+ setActiveValue(firstEnabledTab.value);
42
+ }
43
+ }, [activeValue, setActiveValue, tabs]);
44
+
45
+ const contextValue = React.useMemo(
46
+ () => ({
47
+ activeValue,
48
+ focusedValue,
49
+ getPanelId: (tabValue: string) => resolveTabPanelId(testID, tabValue),
50
+ getTabId: (tabValue: string) => resolveTabId(testID, tabValue),
51
+ registerTab,
52
+ setActiveValue: (nextValue: string) => {
53
+ setActiveValue(nextValue);
54
+ },
55
+ setFocusedValue,
56
+ tabs,
57
+ unregisterTab,
58
+ }),
59
+ [activeValue, focusedValue, registerTab, setActiveValue, tabs, testID, unregisterTab],
60
+ );
61
+
62
+ return (
63
+ <TabsContext.Provider value={contextValue}>
64
+ <Box testID={testID}>{children}</Box>
65
+ </TabsContext.Provider>
66
+ );
67
+ }
@@ -0,0 +1,15 @@
1
+ import { describe, expect, it } from 'bun:test';
2
+
3
+ import { getTabId, getTabPanelId } from './a11y';
4
+
5
+ describe('tabs accessibility ids', () => {
6
+ it('creates stable ids from tab values', () => {
7
+ expect(getTabId(undefined, 'Account Settings')).toBe('tabs-tab-Account-Settings');
8
+ expect(getTabPanelId(undefined, 'Account Settings')).toBe('tabs-panel-Account-Settings');
9
+ });
10
+
11
+ it('namespaces ids by tabs testID when provided', () => {
12
+ expect(getTabId('settings', 'billing')).toBe('settings-tabs-tab-billing');
13
+ expect(getTabPanelId('settings', 'billing')).toBe('settings-tabs-panel-billing');
14
+ });
15
+ });
@@ -0,0 +1,15 @@
1
+ function sanitizeTabValue(value: string): string {
2
+ return value.trim().replace(/[^a-zA-Z0-9_-]+/g, '-');
3
+ }
4
+
5
+ function createTabsIdBase(testID: string | undefined): string {
6
+ return testID ? `${testID}-tabs` : 'tabs';
7
+ }
8
+
9
+ export function getTabId(testID: string | undefined, value: string): string {
10
+ return `${createTabsIdBase(testID)}-tab-${sanitizeTabValue(value)}`;
11
+ }
12
+
13
+ export function getTabPanelId(testID: string | undefined, value: string): string {
14
+ return `${createTabsIdBase(testID)}-panel-${sanitizeTabValue(value)}`;
15
+ }
@@ -0,0 +1,31 @@
1
+ import React from 'react';
2
+
3
+ export interface TabRegistration {
4
+ disabled: boolean;
5
+ focus: () => void;
6
+ value: string;
7
+ }
8
+
9
+ export interface TabsContextValue {
10
+ activeValue: string | undefined;
11
+ focusedValue: string | undefined;
12
+ getPanelId: (value: string) => string;
13
+ getTabId: (value: string) => string;
14
+ registerTab: (tab: TabRegistration) => void;
15
+ setActiveValue: (value: string) => void;
16
+ setFocusedValue: (value: string | undefined) => void;
17
+ tabs: TabRegistration[];
18
+ unregisterTab: (value: string) => void;
19
+ }
20
+
21
+ export const TabsContext = React.createContext<TabsContextValue | null>(null);
22
+
23
+ export function useTabsContext(): TabsContextValue {
24
+ const value = React.useContext(TabsContext);
25
+
26
+ if (!value) {
27
+ throw new Error('Tabs components must be used within <Tabs>.');
28
+ }
29
+
30
+ return value;
31
+ }
@@ -0,0 +1,5 @@
1
+ export { Tab } from './Tab';
2
+ export { TabList } from './TabList';
3
+ export { TabPanel } from './TabPanel';
4
+ export { Tabs } from './Tabs';
5
+ export type { TabListProps, TabPanelProps, TabProps, TabsProps } from './types';
@@ -0,0 +1,21 @@
1
+ import { describe, expect, it } from 'bun:test';
2
+
3
+ import { resolveNextTabValue } from './navigation';
4
+
5
+ const tabs = [
6
+ { disabled: false, focus: () => undefined, value: 'overview' },
7
+ { disabled: true, focus: () => undefined, value: 'disabled' },
8
+ { disabled: false, focus: () => undefined, value: 'details' },
9
+ ];
10
+
11
+ describe('resolveNextTabValue', () => {
12
+ it('skips disabled tabs when cycling', () => {
13
+ expect(resolveNextTabValue(tabs, 'overview', 'ArrowRight')).toBe('details');
14
+ expect(resolveNextTabValue(tabs, 'details', 'ArrowRight')).toBe('overview');
15
+ });
16
+
17
+ it('supports home and end navigation', () => {
18
+ expect(resolveNextTabValue(tabs, 'details', 'Home')).toBe('overview');
19
+ expect(resolveNextTabValue(tabs, 'overview', 'End')).toBe('details');
20
+ });
21
+ });
@@ -0,0 +1,32 @@
1
+ import type { TabRegistration } from './context';
2
+
3
+ export function resolveNextTabValue(
4
+ tabs: TabRegistration[],
5
+ currentValue: string | undefined,
6
+ key: 'ArrowLeft' | 'ArrowRight' | 'ArrowUp' | 'ArrowDown' | 'Home' | 'End',
7
+ ): string | undefined {
8
+ const enabledTabs = tabs.filter((tab) => !tab.disabled);
9
+ if (enabledTabs.length === 0) {
10
+ return undefined;
11
+ }
12
+
13
+ if (key === 'Home') {
14
+ return enabledTabs[0]?.value;
15
+ }
16
+
17
+ if (key === 'End') {
18
+ return enabledTabs[enabledTabs.length - 1]?.value;
19
+ }
20
+
21
+ const currentIndex = enabledTabs.findIndex((tab) => tab.value === currentValue);
22
+ if (currentIndex === -1) {
23
+ return enabledTabs[0]?.value;
24
+ }
25
+
26
+ const nextIndex =
27
+ key === 'ArrowLeft' || key === 'ArrowUp'
28
+ ? (currentIndex - 1 + enabledTabs.length) % enabledTabs.length
29
+ : (currentIndex + 1) % enabledTabs.length;
30
+
31
+ return enabledTabs[nextIndex]?.value;
32
+ }
@@ -0,0 +1,27 @@
1
+ import type React from 'react';
2
+
3
+ export interface TabsProps {
4
+ children?: React.ReactNode;
5
+ value?: string;
6
+ defaultValue?: string;
7
+ onValueChange?: ((value: string) => void) | undefined;
8
+ testID?: string;
9
+ }
10
+
11
+ export interface TabListProps {
12
+ children?: React.ReactNode;
13
+ testID?: string;
14
+ }
15
+
16
+ export interface TabProps {
17
+ value: string;
18
+ children?: React.ReactNode;
19
+ disabled?: boolean;
20
+ testID?: string;
21
+ }
22
+
23
+ export interface TabPanelProps {
24
+ value: string;
25
+ children?: React.ReactNode;
26
+ testID?: string;
27
+ }
@@ -0,0 +1,116 @@
1
+ import React from 'react';
2
+ import { Platform, TextInput as ReactNativeTextInput, View } from 'react-native';
3
+
4
+ import {
5
+ resolveControlSize,
6
+ resolveFieldState,
7
+ resolveFocusRingStyles,
8
+ resolveInputColors,
9
+ resolveTextStyles,
10
+ } from '../../internal/resolvers';
11
+ import { useTheme } from '../../theme/ThemeContext';
12
+ import type { TextInputProps } from './types';
13
+
14
+ export function TextInput({
15
+ value,
16
+ defaultValue,
17
+ onChangeText,
18
+ placeholder,
19
+ size = 'm',
20
+ disabled = false,
21
+ readOnly = false,
22
+ invalid = false,
23
+ leadingAccessory,
24
+ trailingAccessory,
25
+ style,
26
+ testID,
27
+ onFocus,
28
+ onBlur,
29
+ ...props
30
+ }: TextInputProps) {
31
+ const { theme } = useTheme();
32
+ const controlSize = resolveControlSize(theme, size);
33
+ const [focused, setFocused] = React.useState(false);
34
+ const fieldState = resolveFieldState({
35
+ disabled,
36
+ focused,
37
+ invalid,
38
+ readOnly,
39
+ });
40
+ const colors = resolveInputColors(theme, fieldState);
41
+ const resolvedTextStyle = resolveTextStyles(theme, {
42
+ variant: controlSize.textVariant,
43
+ });
44
+ const resolvedLineHeight =
45
+ typeof resolvedTextStyle.lineHeight === 'number'
46
+ ? resolvedTextStyle.lineHeight
47
+ : controlSize.minHeight - controlSize.paddingVertical * 2;
48
+ const lineCount = Math.max(props.numberOfLines ?? 1, 1);
49
+ const inputMinHeight = props.multiline
50
+ ? resolvedLineHeight * lineCount
51
+ : controlSize.minHeight - controlSize.paddingVertical * 2;
52
+ const containerMinHeight = props.multiline
53
+ ? inputMinHeight + controlSize.paddingVertical * 2
54
+ : controlSize.minHeight;
55
+
56
+ const handleFocus: NonNullable<TextInputProps['onFocus']> = (event) => {
57
+ setFocused(true);
58
+ onFocus?.(event);
59
+ };
60
+
61
+ const handleBlur: NonNullable<TextInputProps['onBlur']> = (event) => {
62
+ setFocused(false);
63
+ onBlur?.(event);
64
+ };
65
+
66
+ return (
67
+ <View
68
+ style={[
69
+ {
70
+ minHeight: containerMinHeight,
71
+ paddingHorizontal: controlSize.paddingHorizontal,
72
+ paddingVertical: controlSize.paddingVertical,
73
+ borderRadius: controlSize.borderRadius,
74
+ borderWidth: 1,
75
+ borderColor: colors.borderColor,
76
+ backgroundColor: colors.backgroundColor,
77
+ flexDirection: 'row',
78
+ alignItems: props.multiline ? 'flex-start' : 'center',
79
+ opacity: colors.opacity,
80
+ },
81
+ resolveFocusRingStyles(theme.semantics.border.focus, focused, Platform.OS === 'web'),
82
+ ]}
83
+ >
84
+ {leadingAccessory ? (
85
+ <View style={{ marginRight: theme.spacing.s }}>{leadingAccessory}</View>
86
+ ) : null}
87
+ <ReactNativeTextInput
88
+ {...props}
89
+ defaultValue={defaultValue}
90
+ editable={!disabled && !readOnly}
91
+ numberOfLines={props.multiline ? props.numberOfLines : 1}
92
+ onBlur={handleBlur}
93
+ onChangeText={onChangeText}
94
+ onFocus={handleFocus}
95
+ placeholder={placeholder}
96
+ placeholderTextColor={colors.placeholderColor}
97
+ readOnly={readOnly}
98
+ style={[
99
+ {
100
+ flex: 1,
101
+ minHeight: inputMinHeight,
102
+ padding: 0,
103
+ color: colors.contentColor,
104
+ textAlignVertical: props.multiline ? 'top' : 'center',
105
+ },
106
+ style,
107
+ ]}
108
+ testID={testID}
109
+ value={value}
110
+ />
111
+ {trailingAccessory ? (
112
+ <View style={{ marginLeft: theme.spacing.s }}>{trailingAccessory}</View>
113
+ ) : null}
114
+ </View>
115
+ );
116
+ }
@@ -0,0 +1,2 @@
1
+ export { TextInput } from './TextInput';
2
+ export type { TextInputProps } from './types';