@ankhorage/surface 4.2.1 → 4.4.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 (185) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/README.md +4 -4
  3. package/dist/components/tabs/Tabs.js +2 -2
  4. package/dist/components/tabs/Tabs.js.map +1 -1
  5. package/dist/components/tabs/context.js +1 -1
  6. package/dist/components/tabs/context.js.map +1 -1
  7. package/dist/context/FontContext.d.ts.map +1 -1
  8. package/dist/context/FontContext.js +3 -3
  9. package/dist/context/FontContext.js.map +1 -1
  10. package/dist/context/TranslationContext.d.ts.map +1 -1
  11. package/dist/context/TranslationContext.js +3 -4
  12. package/dist/context/TranslationContext.js.map +1 -1
  13. package/dist/core/responsive/ResponsiveProvider.d.ts.map +1 -1
  14. package/dist/core/responsive/ResponsiveProvider.js +3 -3
  15. package/dist/core/responsive/ResponsiveProvider.js.map +1 -1
  16. package/dist/examples/DocsExamples.js +1 -1
  17. package/dist/examples/DocsExamples.js.map +1 -1
  18. package/dist/features/accordion/adapters/inbound/Accordion.d.ts +5 -0
  19. package/dist/features/accordion/adapters/inbound/Accordion.d.ts.map +1 -0
  20. package/dist/features/accordion/adapters/inbound/Accordion.js +46 -0
  21. package/dist/features/accordion/adapters/inbound/Accordion.js.map +1 -0
  22. package/dist/features/accordion/adapters/inbound/AccordionContent.d.ts +5 -0
  23. package/dist/features/accordion/adapters/inbound/AccordionContent.d.ts.map +1 -0
  24. package/dist/features/accordion/adapters/inbound/AccordionContent.js +16 -0
  25. package/dist/features/accordion/adapters/inbound/AccordionContent.js.map +1 -0
  26. package/dist/features/accordion/adapters/inbound/AccordionContext.d.ts +4 -0
  27. package/dist/features/accordion/adapters/inbound/AccordionContext.d.ts.map +1 -0
  28. package/dist/features/accordion/adapters/inbound/AccordionContext.js +3 -0
  29. package/dist/features/accordion/adapters/inbound/AccordionContext.js.map +1 -0
  30. package/dist/features/accordion/adapters/inbound/AccordionItem.d.ts +5 -0
  31. package/dist/features/accordion/adapters/inbound/AccordionItem.d.ts.map +1 -0
  32. package/dist/features/accordion/adapters/inbound/AccordionItem.js +28 -0
  33. package/dist/features/accordion/adapters/inbound/AccordionItem.js.map +1 -0
  34. package/dist/features/accordion/adapters/inbound/AccordionItemContext.d.ts +4 -0
  35. package/dist/features/accordion/adapters/inbound/AccordionItemContext.d.ts.map +1 -0
  36. package/dist/features/accordion/adapters/inbound/AccordionItemContext.js +3 -0
  37. package/dist/features/accordion/adapters/inbound/AccordionItemContext.js.map +1 -0
  38. package/dist/features/accordion/adapters/inbound/AccordionTrigger.d.ts +5 -0
  39. package/dist/features/accordion/adapters/inbound/AccordionTrigger.d.ts.map +1 -0
  40. package/dist/features/accordion/adapters/inbound/AccordionTrigger.js +17 -0
  41. package/dist/features/accordion/adapters/inbound/AccordionTrigger.js.map +1 -0
  42. package/dist/features/accordion/public.d.ts +6 -0
  43. package/dist/features/accordion/public.d.ts.map +1 -0
  44. package/dist/features/accordion/public.js +5 -0
  45. package/dist/features/accordion/public.js.map +1 -0
  46. package/dist/features/accordion/utils/resolveAccordionToggle.d.ts +11 -0
  47. package/dist/features/accordion/utils/resolveAccordionToggle.d.ts.map +1 -0
  48. package/dist/features/accordion/utils/resolveAccordionToggle.js +13 -0
  49. package/dist/features/accordion/utils/resolveAccordionToggle.js.map +1 -0
  50. package/dist/features/bottom-sheet/BottomSheetProvider.js +2 -2
  51. package/dist/features/bottom-sheet/BottomSheetProvider.js.map +1 -1
  52. package/dist/features/bottom-sheet/useBottomSheet.js +1 -1
  53. package/dist/features/bottom-sheet/useBottomSheet.js.map +1 -1
  54. package/dist/features/popover/adapters/inbound/Popover.d.ts +5 -0
  55. package/dist/features/popover/adapters/inbound/Popover.d.ts.map +1 -0
  56. package/dist/features/popover/adapters/inbound/Popover.js +31 -0
  57. package/dist/features/popover/adapters/inbound/Popover.js.map +1 -0
  58. package/dist/features/popover/adapters/inbound/PopoverOverlay.d.ts +17 -0
  59. package/dist/features/popover/adapters/inbound/PopoverOverlay.d.ts.map +1 -0
  60. package/dist/features/popover/adapters/inbound/PopoverOverlay.js +34 -0
  61. package/dist/features/popover/adapters/inbound/PopoverOverlay.js.map +1 -0
  62. package/dist/features/popover/adapters/inbound/usePopoverLayout.d.ts +21 -0
  63. package/dist/features/popover/adapters/inbound/usePopoverLayout.d.ts.map +1 -0
  64. package/dist/features/popover/adapters/inbound/usePopoverLayout.js +33 -0
  65. package/dist/features/popover/adapters/inbound/usePopoverLayout.js.map +1 -0
  66. package/dist/features/popover/public.d.ts +3 -0
  67. package/dist/features/popover/public.d.ts.map +1 -0
  68. package/dist/features/popover/public.js +2 -0
  69. package/dist/features/popover/public.js.map +1 -0
  70. package/dist/features/popover/utils/resolvePopoverPosition.d.ts +25 -0
  71. package/dist/features/popover/utils/resolvePopoverPosition.d.ts.map +1 -0
  72. package/dist/features/popover/utils/resolvePopoverPosition.js +23 -0
  73. package/dist/features/popover/utils/resolvePopoverPosition.js.map +1 -0
  74. package/dist/{components/toast → features/toast/adapters/inbound}/Toast.d.ts +2 -1
  75. package/dist/features/toast/adapters/inbound/Toast.d.ts.map +1 -0
  76. package/dist/{components/toast → features/toast/adapters/inbound}/Toast.js +37 -32
  77. package/dist/features/toast/adapters/inbound/Toast.js.map +1 -0
  78. package/dist/features/toast/adapters/inbound/ToastProvider.d.ts +5 -0
  79. package/dist/features/toast/adapters/inbound/ToastProvider.d.ts.map +1 -0
  80. package/dist/features/toast/adapters/inbound/ToastProvider.js +29 -0
  81. package/dist/features/toast/adapters/inbound/ToastProvider.js.map +1 -0
  82. package/dist/features/toast/adapters/inbound/useToast.d.ts +4 -0
  83. package/dist/features/toast/adapters/inbound/useToast.d.ts.map +1 -0
  84. package/dist/features/toast/adapters/inbound/useToast.js +11 -0
  85. package/dist/features/toast/adapters/inbound/useToast.js.map +1 -0
  86. package/dist/features/toast/composition/ToastContext.d.ts +4 -0
  87. package/dist/features/toast/composition/ToastContext.d.ts.map +1 -0
  88. package/dist/features/toast/composition/ToastContext.js +4 -0
  89. package/dist/features/toast/composition/ToastContext.js.map +1 -0
  90. package/dist/features/toast/composition/useToastRuntime.d.ts +11 -0
  91. package/dist/features/toast/composition/useToastRuntime.d.ts.map +1 -0
  92. package/dist/features/toast/composition/useToastRuntime.js +50 -0
  93. package/dist/features/toast/composition/useToastRuntime.js.map +1 -0
  94. package/dist/features/toast/public.d.ts +5 -0
  95. package/dist/features/toast/public.d.ts.map +1 -0
  96. package/dist/features/toast/public.js +4 -0
  97. package/dist/features/toast/public.js.map +1 -0
  98. package/dist/index.d.ts +6 -2
  99. package/dist/index.d.ts.map +1 -1
  100. package/dist/index.js +3 -1
  101. package/dist/index.js.map +1 -1
  102. package/dist/internal/overlay/OverlayProvider.d.ts +1 -0
  103. package/dist/internal/overlay/OverlayProvider.d.ts.map +1 -1
  104. package/dist/internal/overlay/OverlayProvider.js +9 -7
  105. package/dist/internal/overlay/OverlayProvider.js.map +1 -1
  106. package/dist/internal/overlay/useOverlayStack.js +1 -1
  107. package/dist/internal/overlay/useOverlayStack.js.map +1 -1
  108. package/dist/internal/resolvers/resolveOverlayZIndex.d.ts +1 -1
  109. package/dist/internal/resolvers/resolveOverlayZIndex.d.ts.map +1 -1
  110. package/dist/internal/resolvers/resolveOverlayZIndex.js +1 -0
  111. package/dist/internal/resolvers/resolveOverlayZIndex.js.map +1 -1
  112. package/dist/layout/AppBar.js +1 -1
  113. package/dist/layout/AppBar.js.map +1 -1
  114. package/dist/theme/ThemeContext.d.ts.map +1 -1
  115. package/dist/theme/ThemeContext.js +4 -5
  116. package/dist/theme/ThemeContext.js.map +1 -1
  117. package/dist/types/accordion.d.ts +54 -0
  118. package/dist/types/accordion.d.ts.map +1 -0
  119. package/dist/types/accordion.js +2 -0
  120. package/dist/types/accordion.js.map +1 -0
  121. package/dist/types/popover.d.ts +21 -0
  122. package/dist/types/popover.d.ts.map +1 -0
  123. package/dist/types/popover.js +2 -0
  124. package/dist/types/popover.js.map +1 -0
  125. package/dist/{components/toast/types.d.ts → types/toast.d.ts} +11 -3
  126. package/dist/types/toast.d.ts.map +1 -0
  127. package/dist/types/toast.js +2 -0
  128. package/dist/types/toast.js.map +1 -0
  129. package/package.json +1 -1
  130. package/src/components/tabs/Tabs.tsx +2 -2
  131. package/src/components/tabs/context.tsx +1 -1
  132. package/src/context/FontContext.tsx +3 -3
  133. package/src/context/TranslationContext.tsx +3 -4
  134. package/src/core/responsive/ResponsiveProvider.tsx +3 -3
  135. package/src/examples/DocsExamples.tsx +1 -1
  136. package/src/features/accordion/adapters/inbound/Accordion.tsx +83 -0
  137. package/src/features/accordion/adapters/inbound/AccordionContent.tsx +30 -0
  138. package/src/features/accordion/adapters/inbound/AccordionContext.ts +5 -0
  139. package/src/features/accordion/adapters/inbound/AccordionItem.tsx +42 -0
  140. package/src/features/accordion/adapters/inbound/AccordionItemContext.ts +5 -0
  141. package/src/features/accordion/adapters/inbound/AccordionTrigger.tsx +34 -0
  142. package/src/features/accordion/public.ts +13 -0
  143. package/src/features/accordion/utils/resolveAccordionToggle.test.ts +57 -0
  144. package/src/features/accordion/utils/resolveAccordionToggle.ts +28 -0
  145. package/src/features/bottom-sheet/BottomSheetProvider.tsx +2 -2
  146. package/src/features/bottom-sheet/useBottomSheet.ts +1 -1
  147. package/src/features/popover/adapters/inbound/Popover.tsx +49 -0
  148. package/src/features/popover/adapters/inbound/PopoverOverlay.tsx +72 -0
  149. package/src/features/popover/adapters/inbound/usePopoverLayout.ts +51 -0
  150. package/src/features/popover/public.ts +2 -0
  151. package/src/features/popover/utils/resolvePopoverPosition.test.ts +31 -0
  152. package/src/features/popover/utils/resolvePopoverPosition.ts +40 -0
  153. package/src/{components/toast → features/toast/adapters/inbound}/Toast.tsx +58 -54
  154. package/src/features/toast/adapters/inbound/ToastProvider.tsx +47 -0
  155. package/src/features/toast/adapters/inbound/useToast.test.tsx +18 -0
  156. package/src/features/toast/adapters/inbound/useToast.ts +15 -0
  157. package/src/features/toast/composition/ToastContext.ts +6 -0
  158. package/src/features/toast/composition/useToastRuntime.ts +76 -0
  159. package/src/features/toast/public.ts +10 -0
  160. package/src/index.test.ts +3 -1
  161. package/src/index.ts +29 -2
  162. package/src/internal/overlay/OverlayProvider.tsx +10 -8
  163. package/src/internal/overlay/useOverlayStack.ts +1 -1
  164. package/src/internal/resolvers/resolveOverlayZIndex.test.ts +2 -1
  165. package/src/internal/resolvers/resolveOverlayZIndex.ts +2 -1
  166. package/src/layout/AppBar.tsx +1 -1
  167. package/src/theme/ThemeContext.tsx +4 -5
  168. package/src/types/accordion.ts +69 -0
  169. package/src/types/popover.ts +25 -0
  170. package/src/{components/toast/types.ts → types/toast.ts} +12 -2
  171. package/dist/components/toast/Toast.d.ts.map +0 -1
  172. package/dist/components/toast/Toast.js.map +0 -1
  173. package/dist/components/toast/ToastProvider.d.ts +0 -13
  174. package/dist/components/toast/ToastProvider.d.ts.map +0 -1
  175. package/dist/components/toast/ToastProvider.js +0 -70
  176. package/dist/components/toast/ToastProvider.js.map +0 -1
  177. package/dist/components/toast/index.d.ts +0 -4
  178. package/dist/components/toast/index.d.ts.map +0 -1
  179. package/dist/components/toast/index.js +0 -3
  180. package/dist/components/toast/index.js.map +0 -1
  181. package/dist/components/toast/types.d.ts.map +0 -1
  182. package/dist/components/toast/types.js +0 -2
  183. package/dist/components/toast/types.js.map +0 -1
  184. package/src/components/toast/ToastProvider.tsx +0 -115
  185. package/src/components/toast/index.ts +0 -3
@@ -60,8 +60,8 @@ export function Tabs({ children, value, defaultValue, onValueChange, testID }: T
60
60
  );
61
61
 
62
62
  return (
63
- <TabsContext.Provider value={contextValue}>
63
+ <TabsContext value={contextValue}>
64
64
  <Box testID={testID}>{children}</Box>
65
- </TabsContext.Provider>
65
+ </TabsContext>
66
66
  );
67
67
  }
@@ -21,7 +21,7 @@ export interface TabsContextValue {
21
21
  export const TabsContext = React.createContext<TabsContextValue | null>(null);
22
22
 
23
23
  export function useTabsContext(): TabsContextValue {
24
- const value = React.useContext(TabsContext);
24
+ const value = React.use(TabsContext);
25
25
 
26
26
  if (!value) {
27
27
  throw new Error('Tabs components must be used within <Tabs>.');
@@ -1,4 +1,4 @@
1
- import React, { createContext, useContext, useMemo, useState } from 'react';
1
+ import React, { createContext, use, useMemo, useState } from 'react';
2
2
 
3
3
  /**
4
4
  * Runtime font state consumed by theme and text primitives.
@@ -51,9 +51,9 @@ export function FontProvider(props: {
51
51
  [fontsLoaded, activeFontId, onActiveFontChange],
52
52
  );
53
53
 
54
- return <FontContext.Provider value={value}>{children}</FontContext.Provider>;
54
+ return <FontContext value={value}>{children}</FontContext>;
55
55
  }
56
56
 
57
57
  export function useFontContext(): FontRuntime {
58
- return useContext(FontContext);
58
+ return use(FontContext);
59
59
  }
@@ -1,4 +1,4 @@
1
- import React, { createContext, useContext } from 'react';
1
+ import React, { createContext, use } from 'react';
2
2
 
3
3
  /**
4
4
  * Minimal translation runtime surface.
@@ -35,7 +35,6 @@ export function TranslationProvider(props: {
35
35
  t: (key: string, options?: Record<string, unknown>) => {
36
36
  if (i18n?.t) {
37
37
  const result = i18n.t(key, options);
38
- // If translation returns the key, it's missing in the current dictionary
39
38
  if (result !== key) return result;
40
39
  }
41
40
  return t(key, options);
@@ -43,9 +42,9 @@ export function TranslationProvider(props: {
43
42
  i18n: i18n ?? null,
44
43
  };
45
44
 
46
- return <TranslationContext.Provider value={value}>{children}</TranslationContext.Provider>;
45
+ return <TranslationContext value={value}>{children}</TranslationContext>;
47
46
  }
48
47
 
49
48
  export function useTranslationContext(): TranslationRuntime {
50
- return useContext(TranslationContext);
49
+ return use(TranslationContext);
51
50
  }
@@ -1,4 +1,4 @@
1
- import React, { createContext, useContext, useMemo } from 'react';
1
+ import React, { createContext, use, useMemo } from 'react';
2
2
  import { useWindowDimensions } from 'react-native';
3
3
 
4
4
  import { getBreakpointFromWidth } from './getBreakpointFromWidth';
@@ -17,11 +17,11 @@ export function ResponsiveProvider({ children }: { children: React.ReactNode })
17
17
  [width],
18
18
  );
19
19
 
20
- return <ResponsiveContext.Provider value={value}>{children}</ResponsiveContext.Provider>;
20
+ return <ResponsiveContext value={value}>{children}</ResponsiveContext>;
21
21
  }
22
22
 
23
23
  export function useResponsiveRuntime(): ResponsiveRuntime {
24
- const runtime = useContext(ResponsiveContext);
24
+ const runtime = use(ResponsiveContext);
25
25
 
26
26
  if (!runtime) {
27
27
  throw new Error('useResponsiveRuntime must be used within a ResponsiveProvider');
@@ -5,13 +5,13 @@ import { HelperText } from '../components/helper-text';
5
5
  import { Menu } from '../components/menu';
6
6
  import { Modal } from '../components/modal';
7
7
  import { Tab, TabList, TabPanel, Tabs } from '../components/tabs';
8
- import { ToastProvider, useToast } from '../components/toast';
9
8
  import { Tooltip } from '../components/tooltip';
10
9
  import { Button } from '../features/button/public';
11
10
  import { Card } from '../features/card/public';
12
11
  import { Checkbox } from '../features/form/checkbox/public';
13
12
  import { TextInput } from '../features/form/text-input/public';
14
13
  import { Box, Stack } from '../features/layout/public';
14
+ import { ToastProvider, useToast } from '../features/toast/public';
15
15
  import { Text } from '../features/typography/public';
16
16
  import { ThemeProvider } from '../theme';
17
17
  import type { ThemeConfig } from '../theme/types';
@@ -0,0 +1,83 @@
1
+ import React from 'react';
2
+ import { View } from 'react-native';
3
+
4
+ import type {
5
+ AccordionMode,
6
+ AccordionMultipleProps,
7
+ AccordionProps,
8
+ AccordionSingleProps,
9
+ } from '../../../../types/accordion';
10
+ import { resolveAccordionToggle } from '../../utils/resolveAccordionToggle';
11
+ import { AccordionContext } from './AccordionContext';
12
+
13
+ /*** Coordinates controlled or uncontrolled accordion expansion state. */
14
+ export function Accordion(props: AccordionProps) {
15
+ const {
16
+ children,
17
+ collapsible = false,
18
+ defaultValue,
19
+ disabled = false,
20
+ interactionPolicy = 'enabled',
21
+ onValueChange: _onValueChange,
22
+ type = 'single',
23
+ value,
24
+ ...viewProps
25
+ } = props;
26
+ const [uncontrolledValues, setUncontrolledValues] = React.useState<readonly string[]>(() =>
27
+ normalizeAccordionValues(type, defaultValue),
28
+ );
29
+ const controlled = value !== undefined;
30
+ const openValues = controlled ? normalizeAccordionValues(type, value) : uncontrolledValues;
31
+
32
+ const toggleValue = React.useCallback(
33
+ (itemValue: string) => {
34
+ if (disabled || interactionPolicy === 'passive') return;
35
+
36
+ const nextValues = resolveAccordionToggle({
37
+ collapsible,
38
+ mode: type,
39
+ openValues,
40
+ value: itemValue,
41
+ });
42
+
43
+ if (!controlled) setUncontrolledValues(nextValues);
44
+ notifyValueChange(props, nextValues);
45
+ },
46
+ [collapsible, controlled, disabled, interactionPolicy, openValues, props, type],
47
+ );
48
+
49
+ const contextValue = React.useMemo(
50
+ () => ({ disabled, interactionPolicy, mode: type, openValues, toggleValue }),
51
+ [disabled, interactionPolicy, openValues, toggleValue, type],
52
+ );
53
+
54
+ return (
55
+ <AccordionContext value={contextValue}>
56
+ <View {...viewProps}>{children}</View>
57
+ </AccordionContext>
58
+ );
59
+ }
60
+
61
+ /*** Normalizes the public single/multiple value shapes for internal state. */
62
+ function normalizeAccordionValues(
63
+ mode: AccordionMode,
64
+ value: string | readonly string[] | undefined,
65
+ ): readonly string[] {
66
+ if (mode === 'multiple') {
67
+ return typeof value === 'string' || value === undefined ? [] : value;
68
+ }
69
+
70
+ return typeof value === 'string' ? [value] : [];
71
+ }
72
+
73
+ /*** Emits the public value shape matching the configured accordion mode. */
74
+ function notifyValueChange(props: AccordionProps, values: readonly string[]) {
75
+ if (props.type === 'multiple') {
76
+ const multipleProps: AccordionMultipleProps = props;
77
+ multipleProps.onValueChange?.(values);
78
+ return;
79
+ }
80
+
81
+ const singleProps: AccordionSingleProps = props;
82
+ singleProps.onValueChange?.(values[0]);
83
+ }
@@ -0,0 +1,30 @@
1
+ import React from 'react';
2
+ import { View, type ViewStyle } from 'react-native';
3
+
4
+ import type { AccordionContentProps } from '../../../../types/accordion';
5
+ import { AccordionItemContext } from './AccordionItemContext';
6
+
7
+ /*** Renders an accordion item's content when expanded or force-mounted. */
8
+ export function AccordionContent({
9
+ children,
10
+ forceMount = false,
11
+ style,
12
+ ...viewProps
13
+ }: AccordionContentProps) {
14
+ const item = React.use(AccordionItemContext);
15
+ if (!item) throw new Error('AccordionContent must be rendered inside AccordionItem.');
16
+ if (!item.open && !forceMount) return null;
17
+
18
+ return (
19
+ <View
20
+ {...viewProps}
21
+ accessibilityLabelledBy={item.triggerId}
22
+ nativeID={item.contentId}
23
+ style={[!item.open ? hiddenContentStyle : undefined, style]}
24
+ >
25
+ {children}
26
+ </View>
27
+ );
28
+ }
29
+
30
+ const hiddenContentStyle: ViewStyle = { display: 'none' };
@@ -0,0 +1,5 @@
1
+ import React from 'react';
2
+
3
+ import type { AccordionContextValue } from '../../../../types/accordion';
4
+
5
+ export const AccordionContext = React.createContext<AccordionContextValue | null>(null);
@@ -0,0 +1,42 @@
1
+ import React from 'react';
2
+ import { View } from 'react-native';
3
+
4
+ import type { AccordionItemProps } from '../../../../types/accordion';
5
+ import { AccordionContext } from './AccordionContext';
6
+ import { AccordionItemContext } from './AccordionItemContext';
7
+
8
+ /*** Provides one accordion item's value, state, and trigger/content identifiers. */
9
+ export function AccordionItem({
10
+ children,
11
+ disabled = false,
12
+ interactionPolicy,
13
+ value,
14
+ ...viewProps
15
+ }: AccordionItemProps) {
16
+ const accordion = React.use(AccordionContext);
17
+ if (!accordion) throw new Error('AccordionItem must be rendered inside Accordion.');
18
+
19
+ const reactId = React.useId();
20
+ const resolvedDisabled = accordion.disabled || disabled;
21
+ const resolvedInteractionPolicy = interactionPolicy ?? accordion.interactionPolicy;
22
+ const open = accordion.openValues.includes(value);
23
+ const toggle = React.useCallback(() => accordion.toggleValue(value), [accordion, value]);
24
+ const contextValue = React.useMemo(
25
+ () => ({
26
+ contentId: `accordion-${reactId}-content`,
27
+ disabled: resolvedDisabled,
28
+ interactionPolicy: resolvedInteractionPolicy,
29
+ open,
30
+ toggle,
31
+ triggerId: `accordion-${reactId}-trigger`,
32
+ value,
33
+ }),
34
+ [open, reactId, resolvedDisabled, resolvedInteractionPolicy, toggle, value],
35
+ );
36
+
37
+ return (
38
+ <AccordionItemContext value={contextValue}>
39
+ <View {...viewProps}>{children}</View>
40
+ </AccordionItemContext>
41
+ );
42
+ }
@@ -0,0 +1,5 @@
1
+ import React from 'react';
2
+
3
+ import type { AccordionItemContextValue } from '../../../../types/accordion';
4
+
5
+ export const AccordionItemContext = React.createContext<AccordionItemContextValue | null>(null);
@@ -0,0 +1,34 @@
1
+ import React from 'react';
2
+ import { Pressable, View } from 'react-native';
3
+
4
+ import type { AccordionTriggerProps } from '../../../../types/accordion';
5
+ import { AccordionItemContext } from './AccordionItemContext';
6
+
7
+ /*** Renders the accessible button that toggles its owning accordion item. */
8
+ export function AccordionTrigger({
9
+ children,
10
+ disabled = false,
11
+ interactionPolicy,
12
+ ...pressableProps
13
+ }: AccordionTriggerProps) {
14
+ const item = React.use(AccordionItemContext);
15
+ if (!item) throw new Error('AccordionTrigger must be rendered inside AccordionItem.');
16
+
17
+ const resolvedDisabled = item.disabled || disabled;
18
+ const resolvedInteractionPolicy = interactionPolicy ?? item.interactionPolicy;
19
+
20
+ return (
21
+ <View accessibilityRole="header">
22
+ <Pressable
23
+ {...pressableProps}
24
+ accessibilityRole="button"
25
+ accessibilityState={{ disabled: resolvedDisabled, expanded: item.open }}
26
+ disabled={resolvedDisabled}
27
+ nativeID={item.triggerId}
28
+ onPress={resolvedInteractionPolicy === 'passive' ? undefined : item.toggle}
29
+ >
30
+ {children}
31
+ </Pressable>
32
+ </View>
33
+ );
34
+ }
@@ -0,0 +1,13 @@
1
+ export type {
2
+ AccordionContentProps,
3
+ AccordionItemProps,
4
+ AccordionMode,
5
+ AccordionMultipleProps,
6
+ AccordionProps,
7
+ AccordionSingleProps,
8
+ AccordionTriggerProps,
9
+ } from '../../types/accordion';
10
+ export { Accordion } from './adapters/inbound/Accordion';
11
+ export { AccordionContent } from './adapters/inbound/AccordionContent';
12
+ export { AccordionItem } from './adapters/inbound/AccordionItem';
13
+ export { AccordionTrigger } from './adapters/inbound/AccordionTrigger';
@@ -0,0 +1,57 @@
1
+ import { describe, expect, test } from 'bun:test';
2
+
3
+ import { resolveAccordionToggle } from './resolveAccordionToggle';
4
+
5
+ describe('resolveAccordionToggle', () => {
6
+ test('opens one value in single mode', () => {
7
+ expect(
8
+ resolveAccordionToggle({
9
+ collapsible: false,
10
+ mode: 'single',
11
+ openValues: ['general'],
12
+ value: 'appearance',
13
+ }),
14
+ ).toEqual(['appearance']);
15
+ });
16
+
17
+ test('keeps the active value open when single mode is not collapsible', () => {
18
+ const openValues = ['general'] as const;
19
+ expect(
20
+ resolveAccordionToggle({
21
+ collapsible: false,
22
+ mode: 'single',
23
+ openValues,
24
+ value: 'general',
25
+ }),
26
+ ).toBe(openValues);
27
+ });
28
+
29
+ test('closes the active value when single mode is collapsible', () => {
30
+ expect(
31
+ resolveAccordionToggle({
32
+ collapsible: true,
33
+ mode: 'single',
34
+ openValues: ['general'],
35
+ value: 'general',
36
+ }),
37
+ ).toEqual([]);
38
+ });
39
+
40
+ test('adds and removes values independently in multiple mode', () => {
41
+ const opened = resolveAccordionToggle({
42
+ collapsible: false,
43
+ mode: 'multiple',
44
+ openValues: ['general'],
45
+ value: 'appearance',
46
+ });
47
+ expect(opened).toEqual(['general', 'appearance']);
48
+ expect(
49
+ resolveAccordionToggle({
50
+ collapsible: false,
51
+ mode: 'multiple',
52
+ openValues: opened,
53
+ value: 'general',
54
+ }),
55
+ ).toEqual(['appearance']);
56
+ });
57
+ });
@@ -0,0 +1,28 @@
1
+ import type { AccordionMode } from '../../../types/accordion';
2
+
3
+ /*** Resolves the next immutable set of open accordion item values. */
4
+ export function resolveAccordionToggle({
5
+ collapsible,
6
+ mode,
7
+ openValues,
8
+ value,
9
+ }: ResolveAccordionToggleInput): readonly string[] {
10
+ if (mode === 'multiple') {
11
+ return openValues.includes(value)
12
+ ? openValues.filter((openValue) => openValue !== value)
13
+ : [...openValues, value];
14
+ }
15
+
16
+ if (openValues.includes(value)) {
17
+ return collapsible ? [] : openValues;
18
+ }
19
+
20
+ return [value];
21
+ }
22
+
23
+ interface ResolveAccordionToggleInput {
24
+ collapsible: boolean;
25
+ mode: AccordionMode;
26
+ openValues: readonly string[];
27
+ value: string;
28
+ }
@@ -48,7 +48,7 @@ export function BottomSheetProvider({ children }: { children: React.ReactNode })
48
48
  );
49
49
 
50
50
  return (
51
- <BottomSheetContext.Provider value={controller}>
51
+ <BottomSheetContext value={controller}>
52
52
  <BottomSheetModalProvider>
53
53
  {children}
54
54
  <BottomSheetHost
@@ -58,7 +58,7 @@ export function BottomSheetProvider({ children }: { children: React.ReactNode })
58
58
  presentedRef={presentedRef}
59
59
  />
60
60
  </BottomSheetModalProvider>
61
- </BottomSheetContext.Provider>
61
+ </BottomSheetContext>
62
62
  );
63
63
  }
64
64
 
@@ -5,7 +5,7 @@ import { BottomSheetContext } from './BottomSheetContext';
5
5
 
6
6
  /*** Returns the shared bottom-sheet controller installed by BottomSheetProvider. */
7
7
  export function useBottomSheet(): BottomSheetController {
8
- const controller = React.useContext(BottomSheetContext);
8
+ const controller = React.use(BottomSheetContext);
9
9
 
10
10
  if (!controller) {
11
11
  throw new Error('useBottomSheet must be used within BottomSheetProvider.');
@@ -0,0 +1,49 @@
1
+ import React from 'react';
2
+ import { View } from 'react-native';
3
+
4
+ import { useControllableState } from '../../../../internal/useControllableState';
5
+ import type { PopoverProps } from '../../../../types/popover';
6
+ import { PopoverOverlay } from './PopoverOverlay';
7
+
8
+ /*** Renders anchored overlay content through the shared Surface overlay stack. */
9
+ export function Popover(props: PopoverProps) {
10
+ const anchorRef = React.useRef<View | null>(null);
11
+ const [resolvedOpen, setResolvedOpen] = useControllableState<boolean>({
12
+ defaultValue: props.defaultOpen ?? false,
13
+ onChange: props.onOpenChange,
14
+ value: props.open,
15
+ });
16
+ const passive = props.interactionPolicy === 'passive';
17
+
18
+ const close = React.useCallback(() => {
19
+ if (!passive) setResolvedOpen(false);
20
+ }, [passive, setResolvedOpen]);
21
+ const toggle = React.useCallback(() => {
22
+ if (!passive) setResolvedOpen(!resolvedOpen);
23
+ }, [passive, resolvedOpen, setResolvedOpen]);
24
+
25
+ return (
26
+ <>
27
+ <View
28
+ collapsable={false}
29
+ ref={anchorRef}
30
+ testID={props.testID ? `${props.testID}-anchor` : undefined}
31
+ >
32
+ {props.anchor({ close, open: resolvedOpen, toggle })}
33
+ </View>
34
+ {resolvedOpen ? (
35
+ <PopoverOverlay
36
+ anchorRef={anchorRef}
37
+ closeOnOutsidePress={props.closeOnOutsidePress ?? true}
38
+ offset={props.offset ?? 8}
39
+ onClose={close}
40
+ passive={passive}
41
+ placement={props.placement ?? 'bottom-start'}
42
+ testID={props.testID}
43
+ >
44
+ {props.children}
45
+ </PopoverOverlay>
46
+ ) : null}
47
+ </>
48
+ );
49
+ }
@@ -0,0 +1,72 @@
1
+ import React from 'react';
2
+ import { Pressable, StyleSheet, View, type ViewStyle } from 'react-native';
3
+
4
+ import { Portal } from '../../../../internal/overlay/Portal';
5
+ import type { PopoverPlacement } from '../../../../types/popover';
6
+ import { usePopoverLayout } from './usePopoverLayout';
7
+
8
+ /*** Renders and positions one mounted popover overlay for an open anchor. */
9
+ export function PopoverOverlay({
10
+ anchorRef,
11
+ children,
12
+ closeOnOutsidePress,
13
+ offset,
14
+ onClose,
15
+ passive,
16
+ placement,
17
+ testID,
18
+ }: PopoverOverlayProps) {
19
+ const { onContentLayout, position, ready } = usePopoverLayout({
20
+ anchorRef,
21
+ offset,
22
+ open: true,
23
+ placement,
24
+ });
25
+
26
+ return (
27
+ <Portal layer="popover" visible={ready}>
28
+ <View pointerEvents="box-none" style={styles.overlay}>
29
+ {closeOnOutsidePress ? (
30
+ <Pressable
31
+ accessibilityLabel="Close popover"
32
+ onPress={passive ? undefined : onClose}
33
+ style={styles.backdrop}
34
+ testID={testID ? `${testID}-backdrop` : undefined}
35
+ />
36
+ ) : null}
37
+ <View
38
+ onLayout={onContentLayout}
39
+ style={[styles.content, position]}
40
+ testID={testID ? `${testID}-content` : undefined}
41
+ >
42
+ {children}
43
+ </View>
44
+ </View>
45
+ </Portal>
46
+ );
47
+ }
48
+
49
+ interface PopoverOverlayProps {
50
+ anchorRef: React.RefObject<View | null>;
51
+ children?: React.ReactNode;
52
+ closeOnOutsidePress: boolean;
53
+ offset: number;
54
+ onClose: () => void;
55
+ passive: boolean;
56
+ placement: PopoverPlacement;
57
+ testID?: string;
58
+ }
59
+
60
+ const absoluteFill: ViewStyle = {
61
+ bottom: 0,
62
+ left: 0,
63
+ position: 'absolute',
64
+ right: 0,
65
+ top: 0,
66
+ };
67
+
68
+ const styles = StyleSheet.create({
69
+ backdrop: absoluteFill,
70
+ content: { position: 'absolute' },
71
+ overlay: absoluteFill,
72
+ });
@@ -0,0 +1,51 @@
1
+ import React from 'react';
2
+ import {
3
+ type LayoutChangeEvent,
4
+ type LayoutRectangle,
5
+ useWindowDimensions,
6
+ type View,
7
+ } from 'react-native';
8
+
9
+ import type { PopoverPlacement } from '../../../../types/popover';
10
+ import { resolvePopoverPosition } from '../../utils/resolvePopoverPosition';
11
+
12
+ /*** Measures the anchor and resolves the current viewport-safe popover position. */
13
+ export function usePopoverLayout({ anchorRef, offset, open, placement }: UsePopoverLayoutInput) {
14
+ const [anchorLayout, setAnchorLayout] = React.useState<LayoutRectangle | null>(null);
15
+ const [contentSize, setContentSize] = React.useState({ height: 0, width: 0 });
16
+ const { height: viewportHeight, width: viewportWidth } = useWindowDimensions();
17
+
18
+ const measureAnchor = React.useCallback(() => {
19
+ anchorRef.current?.measureInWindow((x, y, width, height) => {
20
+ setAnchorLayout({ height, width, x, y });
21
+ });
22
+ }, [anchorRef]);
23
+
24
+ React.useEffect(() => {
25
+ if (open) measureAnchor();
26
+ }, [measureAnchor, open]);
27
+
28
+ const onContentLayout = React.useCallback((event: LayoutChangeEvent) => {
29
+ setContentSize(event.nativeEvent.layout);
30
+ }, []);
31
+
32
+ const position = anchorLayout
33
+ ? resolvePopoverPosition({
34
+ anchor: anchorLayout,
35
+ contentSize,
36
+ offset,
37
+ placement,
38
+ viewport: { height: viewportHeight, width: viewportWidth },
39
+ viewportPadding: 8,
40
+ })
41
+ : undefined;
42
+
43
+ return { measureAnchor, onContentLayout, position, ready: Boolean(anchorLayout) };
44
+ }
45
+
46
+ interface UsePopoverLayoutInput {
47
+ anchorRef: React.RefObject<View | null>;
48
+ offset: number;
49
+ open: boolean;
50
+ placement: PopoverPlacement;
51
+ }
@@ -0,0 +1,2 @@
1
+ export type { PopoverAnchorRenderProps, PopoverPlacement, PopoverProps } from '../../types/popover';
2
+ export { Popover } from './adapters/inbound/Popover';
@@ -0,0 +1,31 @@
1
+ import { describe, expect, it } from 'bun:test';
2
+
3
+ import { resolvePopoverPosition } from './resolvePopoverPosition';
4
+
5
+ describe('resolvePopoverPosition', () => {
6
+ it('places bottom-start content below the anchor', () => {
7
+ expect(
8
+ resolvePopoverPosition({
9
+ anchor: { height: 40, width: 120, x: 100, y: 80 },
10
+ contentSize: { height: 180, width: 240 },
11
+ offset: 8,
12
+ placement: 'bottom-start',
13
+ viewport: { height: 800, width: 1200 },
14
+ viewportPadding: 8,
15
+ }),
16
+ ).toEqual({ left: 100, top: 128 });
17
+ });
18
+
19
+ it('clamps content to the available viewport', () => {
20
+ expect(
21
+ resolvePopoverPosition({
22
+ anchor: { height: 40, width: 80, x: 980, y: 700 },
23
+ contentSize: { height: 240, width: 300 },
24
+ offset: 8,
25
+ placement: 'bottom-end',
26
+ viewport: { height: 800, width: 1200 },
27
+ viewportPadding: 8,
28
+ }),
29
+ ).toEqual({ left: 760, top: 552 });
30
+ });
31
+ });