@korsolutions/ui 0.0.59 → 0.0.61

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 (141) hide show
  1. package/dist/module/components/button/variants/ghost.js +59 -0
  2. package/dist/module/components/button/variants/ghost.js.map +1 -0
  3. package/dist/module/components/button/variants/index.js +3 -1
  4. package/dist/module/components/button/variants/index.js.map +1 -1
  5. package/dist/module/components/icon-button/icon-button.js +63 -0
  6. package/dist/module/components/icon-button/icon-button.js.map +1 -0
  7. package/dist/module/components/icon-button/index.js +4 -0
  8. package/dist/module/components/icon-button/index.js.map +1 -0
  9. package/dist/module/components/icon-button/types.js +4 -0
  10. package/dist/module/components/icon-button/types.js.map +1 -0
  11. package/dist/module/components/icon-button/variants/default.js +34 -0
  12. package/dist/module/components/icon-button/variants/default.js.map +1 -0
  13. package/dist/module/components/icon-button/variants/ghost.js +35 -0
  14. package/dist/module/components/icon-button/variants/ghost.js.map +1 -0
  15. package/dist/module/components/icon-button/variants/index.js +11 -0
  16. package/dist/module/components/icon-button/variants/index.js.map +1 -0
  17. package/dist/module/components/icon-button/variants/secondary.js +37 -0
  18. package/dist/module/components/icon-button/variants/secondary.js.map +1 -0
  19. package/dist/module/components/index.js +2 -0
  20. package/dist/module/components/index.js.map +1 -1
  21. package/dist/module/components/menu/components/menu-checkbox-item.js +4 -4
  22. package/dist/module/components/menu/components/menu-checkbox-item.js.map +1 -1
  23. package/dist/module/components/menu/components/menu-content.js +1 -1
  24. package/dist/module/components/menu/components/menu-radio-item.js +4 -4
  25. package/dist/module/components/menu/components/menu-radio-item.js.map +1 -1
  26. package/dist/module/components/menu/components/menu-selection-indicator.js +29 -0
  27. package/dist/module/components/menu/components/menu-selection-indicator.js.map +1 -0
  28. package/dist/module/components/menu/variants/default.js +8 -17
  29. package/dist/module/components/menu/variants/default.js.map +1 -1
  30. package/dist/module/components/phone-input/components/country-picker.js +1 -1
  31. package/dist/module/components/popover/components/popover-content.js +2 -5
  32. package/dist/module/components/popover/components/popover-content.js.map +1 -1
  33. package/dist/module/components/portal/index.js +1 -0
  34. package/dist/module/components/portal/index.js.map +1 -1
  35. package/dist/module/components/portal/portal-offset.js +32 -0
  36. package/dist/module/components/portal/portal-offset.js.map +1 -0
  37. package/dist/module/components/portal/portal.js +39 -17
  38. package/dist/module/components/portal/portal.js.map +1 -1
  39. package/dist/module/components/select/components/select-content.js +3 -3
  40. package/dist/module/components/select/components/select-content.js.map +1 -1
  41. package/dist/module/components/separator/index.js +4 -0
  42. package/dist/module/components/separator/index.js.map +1 -0
  43. package/dist/module/components/separator/separator.js +17 -0
  44. package/dist/module/components/separator/separator.js.map +1 -0
  45. package/dist/module/components/separator/types.js +4 -0
  46. package/dist/module/components/separator/types.js.map +1 -0
  47. package/dist/module/components/separator/variants/horizontal.js +15 -0
  48. package/dist/module/components/separator/variants/horizontal.js.map +1 -0
  49. package/dist/module/components/separator/variants/index.js +9 -0
  50. package/dist/module/components/separator/variants/index.js.map +1 -0
  51. package/dist/module/components/separator/variants/vertical.js +15 -0
  52. package/dist/module/components/separator/variants/vertical.js.map +1 -0
  53. package/dist/module/components/textarea/variants/default.js +7 -0
  54. package/dist/module/components/textarea/variants/default.js.map +1 -1
  55. package/dist/module/hooks/use-relative-position.js +37 -28
  56. package/dist/module/hooks/use-relative-position.js.map +1 -1
  57. package/dist/module/themes/provider.js.map +1 -1
  58. package/dist/typescript/src/components/button/variants/ghost.d.ts +3 -0
  59. package/dist/typescript/src/components/button/variants/ghost.d.ts.map +1 -0
  60. package/dist/typescript/src/components/button/variants/index.d.ts +1 -0
  61. package/dist/typescript/src/components/button/variants/index.d.ts.map +1 -1
  62. package/dist/typescript/src/components/icon-button/icon-button.d.ts +15 -0
  63. package/dist/typescript/src/components/icon-button/icon-button.d.ts.map +1 -0
  64. package/dist/typescript/src/components/icon-button/index.d.ts +3 -0
  65. package/dist/typescript/src/components/icon-button/index.d.ts.map +1 -0
  66. package/dist/typescript/src/components/icon-button/types.d.ts +8 -0
  67. package/dist/typescript/src/components/icon-button/types.d.ts.map +1 -0
  68. package/dist/typescript/src/components/icon-button/variants/default.d.ts +3 -0
  69. package/dist/typescript/src/components/icon-button/variants/default.d.ts.map +1 -0
  70. package/dist/typescript/src/components/icon-button/variants/ghost.d.ts +3 -0
  71. package/dist/typescript/src/components/icon-button/variants/ghost.d.ts.map +1 -0
  72. package/dist/typescript/src/components/icon-button/variants/index.d.ts +6 -0
  73. package/dist/typescript/src/components/icon-button/variants/index.d.ts.map +1 -0
  74. package/dist/typescript/src/components/icon-button/variants/secondary.d.ts +3 -0
  75. package/dist/typescript/src/components/icon-button/variants/secondary.d.ts.map +1 -0
  76. package/dist/typescript/src/components/index.d.ts +2 -0
  77. package/dist/typescript/src/components/index.d.ts.map +1 -1
  78. package/dist/typescript/src/components/menu/components/menu-checkbox-item.d.ts.map +1 -1
  79. package/dist/typescript/src/components/menu/components/menu-radio-item.d.ts.map +1 -1
  80. package/dist/typescript/src/components/menu/components/menu-selection-indicator.d.ts +7 -0
  81. package/dist/typescript/src/components/menu/components/menu-selection-indicator.d.ts.map +1 -0
  82. package/dist/typescript/src/components/menu/types.d.ts +1 -3
  83. package/dist/typescript/src/components/menu/types.d.ts.map +1 -1
  84. package/dist/typescript/src/components/menu/variants/default.d.ts.map +1 -1
  85. package/dist/typescript/src/components/popover/components/popover-content.d.ts.map +1 -1
  86. package/dist/typescript/src/components/portal/index.d.ts +1 -0
  87. package/dist/typescript/src/components/portal/index.d.ts.map +1 -1
  88. package/dist/typescript/src/components/portal/portal-offset.d.ts +13 -0
  89. package/dist/typescript/src/components/portal/portal-offset.d.ts.map +1 -0
  90. package/dist/typescript/src/components/portal/portal.d.ts +3 -2
  91. package/dist/typescript/src/components/portal/portal.d.ts.map +1 -1
  92. package/dist/typescript/src/components/select/components/select-content.d.ts.map +1 -1
  93. package/dist/typescript/src/components/separator/index.d.ts +3 -0
  94. package/dist/typescript/src/components/separator/index.d.ts.map +1 -0
  95. package/dist/typescript/src/components/separator/separator.d.ts +9 -0
  96. package/dist/typescript/src/components/separator/separator.d.ts.map +1 -0
  97. package/dist/typescript/src/components/separator/types.d.ts +5 -0
  98. package/dist/typescript/src/components/separator/types.d.ts.map +1 -0
  99. package/dist/typescript/src/components/separator/variants/horizontal.d.ts +3 -0
  100. package/dist/typescript/src/components/separator/variants/horizontal.d.ts.map +1 -0
  101. package/dist/typescript/src/components/separator/variants/index.d.ts +5 -0
  102. package/dist/typescript/src/components/separator/variants/index.d.ts.map +1 -0
  103. package/dist/typescript/src/components/separator/variants/vertical.d.ts +3 -0
  104. package/dist/typescript/src/components/separator/variants/vertical.d.ts.map +1 -0
  105. package/dist/typescript/src/components/textarea/variants/default.d.ts.map +1 -1
  106. package/dist/typescript/src/hooks/use-relative-position.d.ts +4 -7
  107. package/dist/typescript/src/hooks/use-relative-position.d.ts.map +1 -1
  108. package/dist/typescript/src/themes/provider.d.ts +3 -0
  109. package/dist/typescript/src/themes/provider.d.ts.map +1 -1
  110. package/package.json +1 -1
  111. package/src/components/button/variants/ghost.tsx +55 -0
  112. package/src/components/button/variants/index.ts +2 -0
  113. package/src/components/icon-button/icon-button.tsx +92 -0
  114. package/src/components/icon-button/index.ts +2 -0
  115. package/src/components/icon-button/types.ts +9 -0
  116. package/src/components/icon-button/variants/default.tsx +32 -0
  117. package/src/components/icon-button/variants/ghost.tsx +33 -0
  118. package/src/components/icon-button/variants/index.ts +9 -0
  119. package/src/components/icon-button/variants/secondary.tsx +35 -0
  120. package/src/components/index.ts +2 -0
  121. package/src/components/menu/components/menu-checkbox-item.tsx +3 -4
  122. package/src/components/menu/components/menu-content.tsx +1 -1
  123. package/src/components/menu/components/menu-radio-item.tsx +3 -7
  124. package/src/components/menu/components/menu-selection-indicator.tsx +26 -0
  125. package/src/components/menu/types.ts +1 -6
  126. package/src/components/menu/variants/default.tsx +7 -16
  127. package/src/components/phone-input/components/country-picker.tsx +1 -1
  128. package/src/components/popover/components/popover-content.tsx +1 -4
  129. package/src/components/portal/index.ts +1 -0
  130. package/src/components/portal/portal-offset.ts +28 -0
  131. package/src/components/portal/portal.tsx +54 -22
  132. package/src/components/select/components/select-content.tsx +14 -5
  133. package/src/components/separator/index.ts +2 -0
  134. package/src/components/separator/separator.tsx +15 -0
  135. package/src/components/separator/types.ts +5 -0
  136. package/src/components/separator/variants/horizontal.tsx +14 -0
  137. package/src/components/separator/variants/index.ts +7 -0
  138. package/src/components/separator/variants/vertical.tsx +14 -0
  139. package/src/components/textarea/variants/default.tsx +7 -0
  140. package/src/hooks/use-relative-position.ts +53 -41
  141. package/src/themes/provider.tsx +3 -0
@@ -0,0 +1,26 @@
1
+ import React from "react";
2
+ import { Text, View } from "react-native";
3
+ import { useComponentsConfig } from "../../../themes";
4
+ import { useMenu } from "../context";
5
+
6
+ interface MenuSelectionIndicatorProps {
7
+ isSelected: boolean;
8
+ }
9
+
10
+ export function MenuSelectionIndicator({
11
+ isSelected,
12
+ }: MenuSelectionIndicatorProps) {
13
+ const config = useComponentsConfig();
14
+ const menu = useMenu();
15
+ const SelectionIcon = config?.menu?.selectionIcon;
16
+
17
+ if (!isSelected) {
18
+ return <View style={menu.styles?.selectionIndicator} />;
19
+ }
20
+
21
+ if (SelectionIcon) {
22
+ return <SelectionIcon {...menu.styles?.selectionIndicator} />;
23
+ }
24
+
25
+ return <Text style={menu.styles?.selectionIndicator}>✓</Text>;
26
+ }
@@ -11,8 +11,6 @@ export type MenuRadioItemState =
11
11
  | "selected"
12
12
  | "disabled";
13
13
 
14
- export type MenuRadioIndicatorState = "default" | "selected";
15
-
16
14
  export interface MenuStyles {
17
15
  content?: MenuContentProps["style"];
18
16
  item?: Partial<Record<MenuButtonState, StyleProp<ViewStyle>>>;
@@ -24,11 +22,8 @@ export interface MenuStyles {
24
22
  label?: StyleProp<TextStyle>;
25
23
  separator?: StyleProp<ViewStyle>;
26
24
  checkboxItem?: Partial<Record<MenuCheckboxItemState, StyleProp<ViewStyle>>>;
27
- checkboxIndicator?: StyleProp<TextStyle>;
25
+ selectionIndicator?: TextStyle & SvgProps;
28
26
  radioGroup?: StyleProp<ViewStyle>;
29
27
  radioItem?: Partial<Record<MenuRadioItemState, StyleProp<ViewStyle>>>;
30
- radioIndicator?: Partial<
31
- Record<MenuRadioIndicatorState, StyleProp<ViewStyle>>
32
- >;
33
28
  shortcut?: StyleProp<TextStyle>;
34
29
  }
@@ -67,13 +67,16 @@ export const useMenuVariantDefault = (): MenuStyles => {
67
67
  opacity: 0.5,
68
68
  },
69
69
  },
70
- checkboxIndicator: {
71
- fontSize: fontSize * 0.75,
72
- fontWeight: "bold",
70
+ selectionIndicator: {
73
71
  color: colors.foreground,
72
+ fontSize: fontSize,
74
73
  width: fontSize,
75
- textAlign: "center",
74
+ size: fontSize,
75
+ strokeWidth: 2,
76
76
  marginLeft: "auto",
77
+ style: {
78
+ marginLeft: "auto",
79
+ },
77
80
  },
78
81
  radioItem: {
79
82
  default: {
@@ -90,18 +93,6 @@ export const useMenuVariantDefault = (): MenuStyles => {
90
93
  opacity: 0.5,
91
94
  },
92
95
  },
93
- radioIndicator: {
94
- default: {
95
- width: 8,
96
- height: 8,
97
- borderRadius: 4,
98
- backgroundColor: "transparent",
99
- marginLeft: "auto",
100
- },
101
- selected: {
102
- backgroundColor: colors.foreground,
103
- },
104
- },
105
96
  shortcut: {
106
97
  fontSize: fontSize * 0.75,
107
98
  fontFamily: fontFamily,
@@ -64,7 +64,7 @@ export function CountryPicker() {
64
64
  contentLayout,
65
65
  alignOffset: 0,
66
66
  preferredSide: "bottom",
67
- sideOffset: 4,
67
+ sideOffset: 2,
68
68
  });
69
69
 
70
70
  const countryButtonStyles = StyleSheet.flatten([
@@ -1,7 +1,6 @@
1
- import { useRelativePosition } from "../../../hooks/use-relative-position";
2
- import { useSafeAreaInsets } from "../../../safe-area";
3
1
  import React from "react";
4
2
  import { type StyleProp, View, type ViewStyle } from "react-native";
3
+ import { useRelativePosition } from "../../../hooks/use-relative-position";
5
4
  import { usePopover } from "../context";
6
5
 
7
6
  export interface PopoverContentProps {
@@ -14,7 +13,6 @@ export interface PopoverContentProps {
14
13
 
15
14
  export function PopoverContent(props: PopoverContentProps) {
16
15
  const popover = usePopover();
17
- const insets = useSafeAreaInsets();
18
16
 
19
17
  const positionStyle = useRelativePosition({
20
18
  align: "start",
@@ -23,7 +21,6 @@ export function PopoverContent(props: PopoverContentProps) {
23
21
  alignOffset: 0,
24
22
  preferredSide: "bottom",
25
23
  sideOffset: 0,
26
- insets,
27
24
  });
28
25
 
29
26
  const composedStyle = [positionStyle, popover.styles?.content, props.style];
@@ -1,2 +1,3 @@
1
1
  export * from "./portal";
2
+ export * from "./portal-offset";
2
3
  export * from "./portal.constants";
@@ -0,0 +1,28 @@
1
+ import { createContext, useContext } from "react";
2
+
3
+ export interface PortalOffset {
4
+ x: number;
5
+ y: number;
6
+ }
7
+
8
+ export type PortalOffsetValue = PortalOffset | null | undefined;
9
+
10
+ // null means the offset is not measure yet but will be
11
+ // undefined means the offset is not available and will never be (e.g. on web or ios when FullWindowOverlay is used)
12
+ export const PortalOffsetContext = createContext<PortalOffsetValue>(undefined);
13
+
14
+ type UsePortalOffsetReturn = {
15
+ value: PortalOffset;
16
+ isLoaded: boolean;
17
+ };
18
+
19
+ export function usePortalOffset(): UsePortalOffsetReturn {
20
+ const value = useContext(PortalOffsetContext);
21
+ if (value === undefined) {
22
+ return { value: { x: 0, y: 0 }, isLoaded: true };
23
+ }
24
+ if (value === null) {
25
+ return { value: { x: 0, y: 0 }, isLoaded: false };
26
+ }
27
+ return { value, isLoaded: true };
28
+ }
@@ -1,5 +1,13 @@
1
- import { useEffect, useSyncExternalStore } from "react";
2
- import { Platform, View } from "react-native";
1
+ import React, {
2
+ useCallback,
3
+ useEffect,
4
+ useMemo,
5
+ useRef,
6
+ useState,
7
+ useSyncExternalStore,
8
+ } from "react";
9
+ import { Platform, View, type HostInstance } from "react-native";
10
+ import { PortalOffsetContext, type PortalOffset } from "./portal-offset";
3
11
  import {
4
12
  DEFAULT_PORTAL_HOST,
5
13
  type PortalHostProps,
@@ -59,33 +67,57 @@ function removePortal(hostName: string, name: string) {
59
67
  emit();
60
68
  }
61
69
 
70
+ function DefaultContainer(props: React.PropsWithChildren) {
71
+ const containerRef = useRef<HostInstance>(null);
72
+ const [offset, setOffset] = useState<PortalOffset | null>(null);
73
+
74
+ const onLayout = useCallback(() => {
75
+ containerRef.current?.measureInWindow((pageX: number, pageY: number) => {
76
+ setOffset((prev) => {
77
+ if (prev?.x === pageX && prev?.y === pageY) return prev;
78
+ return { x: pageX, y: pageY };
79
+ });
80
+ });
81
+ }, []);
82
+
83
+ return (
84
+ <View
85
+ ref={containerRef}
86
+ onLayout={onLayout}
87
+ style={{
88
+ position: "absolute",
89
+ top: 0,
90
+ left: 0,
91
+ right: 0,
92
+ bottom: 0,
93
+ elevation: 999,
94
+ zIndex: 999,
95
+ pointerEvents: "box-none",
96
+ }}
97
+ >
98
+ <PortalOffsetContext.Provider value={offset}>
99
+ {props.children}
100
+ </PortalOffsetContext.Provider>
101
+ </View>
102
+ );
103
+ }
104
+
62
105
  export function PortalHost({
63
106
  name = DEFAULT_PORTAL_HOST,
64
107
  container,
65
108
  }: PortalHostProps) {
66
109
  const map = useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
67
110
  const portalMap = map.get(name) ?? new Map<string, React.ReactNode>();
68
- if (portalMap.size === 0) return null;
69
-
70
- const Container = Platform.select({
71
- default: (props: React.PropsWithChildren) => (
72
- <View
73
- {...props}
74
- style={{
75
- position: "absolute",
76
- top: 0,
77
- left: 0,
78
- right: 0,
79
- bottom: 0,
80
- elevation: 999,
81
- zIndex: 999,
82
- pointerEvents: "box-none",
83
- }}
84
- />
85
- ),
86
- ...container,
87
- });
111
+ const Container = useMemo(
112
+ () =>
113
+ Platform.select({
114
+ default: DefaultContainer,
115
+ ...container,
116
+ }),
117
+ [container],
118
+ );
88
119
 
120
+ if (portalMap.size === 0) return null;
89
121
  return <Container>{Array.from(portalMap.values())}</Container>;
90
122
  }
91
123
 
@@ -1,7 +1,7 @@
1
- import { useRelativePosition } from "../../../hooks/use-relative-position";
2
- import { calculateComposedStyles } from "../../../utils/calculate-styles";
3
1
  import React from "react";
4
2
  import { type StyleProp, View, type ViewStyle } from "react-native";
3
+ import { useRelativePosition } from "../../../hooks/use-relative-position";
4
+ import { calculateComposedStyles } from "../../../utils/calculate-styles";
5
5
  import { useSelect } from "../context";
6
6
 
7
7
  export interface SelectContentProps {
@@ -14,7 +14,12 @@ export interface SelectContentProps {
14
14
 
15
15
  export function SelectContent(props: SelectContentProps) {
16
16
  const select = useSelect();
17
- const composedStyles = calculateComposedStyles(select.styles, select.state, "content", props.style);
17
+ const composedStyles = calculateComposedStyles(
18
+ select.styles,
19
+ select.state,
20
+ "content",
21
+ props.style,
22
+ );
18
23
 
19
24
  const positionStyle = useRelativePosition({
20
25
  align: "start",
@@ -22,13 +27,17 @@ export function SelectContent(props: SelectContentProps) {
22
27
  contentLayout: select.contentLayout,
23
28
  alignOffset: 0,
24
29
  preferredSide: "bottom",
25
- sideOffset: 0,
30
+ sideOffset: 2,
26
31
  });
27
32
 
28
33
  const Component = props.render ?? View;
29
34
  return (
30
35
  <Component
31
- style={[positionStyle, composedStyles, { width: select.triggerPosition.width }]}
36
+ style={[
37
+ positionStyle,
38
+ composedStyles,
39
+ { width: select.triggerPosition.width },
40
+ ]}
32
41
  onLayout={(e) => {
33
42
  select.setContentLayout(e.nativeEvent.layout);
34
43
  }}
@@ -0,0 +1,2 @@
1
+ export { Separator, type SeparatorProps } from "./separator";
2
+ export type { SeparatorStyles } from "./types";
@@ -0,0 +1,15 @@
1
+ import React from "react";
2
+ import { View, type StyleProp, type ViewStyle } from "react-native";
3
+ import { SeparatorVariants } from "./variants";
4
+
5
+ export interface SeparatorProps {
6
+ variant?: keyof typeof SeparatorVariants;
7
+ style?: StyleProp<ViewStyle>;
8
+ }
9
+
10
+ export function Separator(props: SeparatorProps) {
11
+ const { variant = "horizontal", style } = props;
12
+ const variantStyles = SeparatorVariants[variant]();
13
+
14
+ return <View style={[variantStyles.root, style]} />;
15
+ }
@@ -0,0 +1,5 @@
1
+ import type { StyleProp, ViewStyle } from "react-native";
2
+
3
+ export interface SeparatorStyles {
4
+ root?: StyleProp<ViewStyle>;
5
+ }
@@ -0,0 +1,14 @@
1
+ import { useThemedStyles } from "../../../utils/use-themed-styles";
2
+ import type { SeparatorStyles } from "../types";
3
+
4
+ export const useSeparatorVariantHorizontal = (): SeparatorStyles => {
5
+ return useThemedStyles(
6
+ ({ colors }): SeparatorStyles => ({
7
+ root: {
8
+ height: 1,
9
+ alignSelf: "stretch",
10
+ backgroundColor: colors.border,
11
+ },
12
+ }),
13
+ );
14
+ };
@@ -0,0 +1,7 @@
1
+ import { useSeparatorVariantHorizontal } from "./horizontal";
2
+ import { useSeparatorVariantVertical } from "./vertical";
3
+
4
+ export const SeparatorVariants = {
5
+ horizontal: useSeparatorVariantHorizontal,
6
+ vertical: useSeparatorVariantVertical,
7
+ };
@@ -0,0 +1,14 @@
1
+ import { useThemedStyles } from "../../../utils/use-themed-styles";
2
+ import type { SeparatorStyles } from "../types";
3
+
4
+ export const useSeparatorVariantVertical = (): SeparatorStyles => {
5
+ return useThemedStyles(
6
+ ({ colors }): SeparatorStyles => ({
7
+ root: {
8
+ width: 1,
9
+ alignSelf: "stretch",
10
+ backgroundColor: colors.border,
11
+ },
12
+ }),
13
+ );
14
+ };
@@ -1,3 +1,4 @@
1
+ import { Platform } from "react-native";
1
2
  import { type TextareaStyles } from "../..";
2
3
  import { useThemedStyles } from "../../../utils/use-themed-styles";
3
4
 
@@ -15,6 +16,12 @@ export function useTextareaVariantDefault(): TextareaStyles {
15
16
  paddingVertical: 12,
16
17
  paddingHorizontal: 16,
17
18
  outlineWidth: 0,
19
+ ...Platform.select({
20
+ default: {},
21
+ web: {
22
+ outline: "none",
23
+ },
24
+ }),
18
25
  fontFamily,
19
26
  fontSize,
20
27
  minHeight: 120,
@@ -5,29 +5,35 @@ import {
5
5
  type LayoutRectangle,
6
6
  type ViewStyle,
7
7
  } from "react-native";
8
- import type { SafeAreaInsets } from "../safe-area";
8
+ import { usePortalOffset } from "../components/portal";
9
+ import { useSafeAreaInsets, type SafeAreaInsets } from "../safe-area";
9
10
 
10
11
  type UseRelativePositionArgs = Omit<
11
12
  GetContentStyleArgs,
12
- "triggerPosition" | "contentLayout" | "dimensions"
13
- > & {
14
- triggerPosition: LayoutPosition | null;
15
- contentLayout: LayoutRectangle | null;
16
- };
13
+ "dimensions" | "insets"
14
+ >;
17
15
 
18
16
  export function useRelativePosition({
19
17
  align,
20
18
  triggerPosition,
21
19
  contentLayout,
22
20
  alignOffset,
23
- insets,
24
21
  sideOffset,
25
22
  preferredSide,
26
23
  }: UseRelativePositionArgs): ViewStyle {
27
24
  const dimensions = useWindowDimensions();
25
+ const insets = useSafeAreaInsets();
26
+ const portalOffset = usePortalOffset();
28
27
 
29
28
  return useMemo(() => {
30
- if (!triggerPosition || !contentLayout) {
29
+ const hasLayout =
30
+ triggerPosition.width > 0 &&
31
+ triggerPosition.height > 0 &&
32
+ contentLayout.width > 0 &&
33
+ contentLayout.height > 0 &&
34
+ portalOffset.isLoaded;
35
+
36
+ if (!hasLayout) {
31
37
  return {
32
38
  position: "absolute",
33
39
  opacity: 0,
@@ -36,11 +42,22 @@ export function useRelativePosition({
36
42
  };
37
43
  }
38
44
 
45
+ // Adjust trigger position to account for the portal container's
46
+ // offset from the window origin. measureInWindow returns window-relative
47
+ // coordinates, but the portal content is positioned relative to its
48
+ // container which may not be at the window origin (e.g. on Android
49
+ // where the status bar offsets the view hierarchy).
50
+ const adjustedTriggerPosition: LayoutPosition = {
51
+ ...triggerPosition,
52
+ pageX: triggerPosition.pageX - portalOffset.value.x,
53
+ pageY: triggerPosition.pageY - portalOffset.value.y,
54
+ };
55
+
39
56
  const style = getContentStyle({
40
57
  align,
41
58
  contentLayout,
42
59
  preferredSide,
43
- triggerPosition,
60
+ triggerPosition: adjustedTriggerPosition,
44
61
  alignOffset,
45
62
  insets,
46
63
  sideOffset,
@@ -58,6 +75,7 @@ export function useRelativePosition({
58
75
  dimensions.width,
59
76
  dimensions.height,
60
77
  sideOffset,
78
+ portalOffset,
61
79
  ]);
62
80
  }
63
81
 
@@ -72,7 +90,7 @@ interface GetPositionArgs {
72
90
  dimensions: DisplayMetrics;
73
91
  triggerPosition: LayoutPosition;
74
92
  contentLayout: LayoutRectangle;
75
- insets?: SafeAreaInsets;
93
+ insets: SafeAreaInsets;
76
94
  }
77
95
 
78
96
  interface GetSidePositionArgs extends GetPositionArgs {
@@ -95,8 +113,7 @@ export const DEFAULT_POSITION: LayoutPosition = {
95
113
 
96
114
  interface GetSideArgs {
97
115
  preferredSide: "top" | "bottom";
98
- insetTop: number;
99
- insetBottom: number;
116
+ insets: SafeAreaInsets;
100
117
  positionTop: number;
101
118
  positionBottom: number;
102
119
  contentLayout: LayoutRectangle;
@@ -105,29 +122,28 @@ interface GetSideArgs {
105
122
 
106
123
  function getSide({
107
124
  preferredSide,
108
- insetTop,
109
- insetBottom,
125
+ insets,
110
126
  positionTop,
111
127
  positionBottom,
112
128
  contentLayout,
113
129
  dimensions,
114
130
  }: GetSideArgs) {
115
131
  if (preferredSide === "bottom") {
116
- const spaceBelow = dimensions.height - insetBottom - positionBottom;
132
+ const spaceBelow = dimensions.height - insets.bottom - positionBottom;
117
133
  if (spaceBelow >= contentLayout.height) {
118
134
  return "bottom";
119
135
  }
120
- const spaceAbove = positionTop - insetTop;
136
+ const spaceAbove = positionTop - insets.top;
121
137
  if (spaceAbove > spaceBelow) {
122
138
  return "top";
123
139
  }
124
140
  return "bottom";
125
141
  }
126
- const spaceAbove = positionTop - insetTop;
142
+ const spaceAbove = positionTop - insets.top;
127
143
  if (spaceAbove >= contentLayout.height) {
128
144
  return "top";
129
145
  }
130
- const spaceBelow = dimensions.height - insetBottom - positionBottom;
146
+ const spaceBelow = dimensions.height - insets.bottom - positionBottom;
131
147
  if (spaceBelow > spaceAbove) {
132
148
  return "bottom";
133
149
  }
@@ -142,8 +158,6 @@ function getSidePosition({
142
158
  insets,
143
159
  dimensions,
144
160
  }: GetSidePositionArgs) {
145
- const insetTop = insets?.top ?? 0;
146
- const insetBottom = insets?.bottom ?? 0;
147
161
  const positionTop =
148
162
  triggerPosition?.pageY - sideOffset - contentLayout.height;
149
163
  const positionBottom =
@@ -151,8 +165,7 @@ function getSidePosition({
151
165
 
152
166
  const side = getSide({
153
167
  preferredSide,
154
- insetTop,
155
- insetBottom,
168
+ insets,
156
169
  positionTop,
157
170
  positionBottom,
158
171
  contentLayout,
@@ -162,15 +175,15 @@ function getSidePosition({
162
175
  if (side === "top") {
163
176
  return {
164
177
  top: Math.min(
165
- Math.max(insetTop, positionTop),
166
- dimensions.height - insetBottom - contentLayout.height,
178
+ Math.max(insets.top, positionTop),
179
+ dimensions.height - insets.bottom - contentLayout.height,
167
180
  ),
168
181
  };
169
182
  }
170
183
 
171
184
  return {
172
185
  top: Math.min(
173
- dimensions.height - insetBottom - contentLayout.height,
186
+ dimensions.height - insets.bottom - contentLayout.height,
174
187
  positionBottom,
175
188
  ),
176
189
  };
@@ -189,9 +202,7 @@ function getAlignPosition({
189
202
  insets,
190
203
  dimensions,
191
204
  }: GetAlignPositionArgs) {
192
- const insetLeft = insets?.left ?? 0;
193
- const insetRight = insets?.right ?? 0;
194
- const maxContentWidth = dimensions.width - insetLeft - insetRight;
205
+ const maxContentWidth = dimensions.width - insets.left - insets.right;
195
206
 
196
207
  const contentWidth = Math.min(contentLayout.width, maxContentWidth);
197
208
 
@@ -201,25 +212,24 @@ function getAlignPosition({
201
212
  triggerPosition.width,
202
213
  contentWidth,
203
214
  alignOffset,
204
- insetLeft,
205
- insetRight,
215
+ insets,
206
216
  dimensions,
207
217
  );
208
218
 
209
219
  const doesCollide =
210
- left < insetLeft || left + contentWidth > dimensions.width - insetRight;
220
+ left < insets.left || left + contentWidth > dimensions.width - insets.right;
211
221
  if (doesCollide) {
212
- const spaceLeft = left - insetLeft;
213
- const spaceRight = dimensions.width - insetRight - (left + contentWidth);
222
+ const spaceLeft = left - insets.left;
223
+ const spaceRight = dimensions.width - insets.right - (left + contentWidth);
214
224
 
215
225
  if (spaceLeft > spaceRight && spaceLeft >= contentWidth) {
216
- left = insetLeft;
226
+ left = insets.left;
217
227
  } else if (spaceRight >= contentWidth) {
218
- left = dimensions.width - insetRight - contentWidth;
228
+ left = dimensions.width - insets.right - contentWidth;
219
229
  } else {
220
230
  const centeredPosition = Math.max(
221
- insetLeft,
222
- (dimensions.width - contentWidth - insetRight) / 2,
231
+ insets.left,
232
+ (dimensions.width - contentWidth - insets.right) / 2,
223
233
  );
224
234
  left = centeredPosition;
225
235
  }
@@ -234,8 +244,7 @@ function getLeftPosition(
234
244
  triggerWidth: number,
235
245
  contentWidth: number,
236
246
  alignOffset: number,
237
- insetLeft: number,
238
- insetRight: number,
247
+ insets: SafeAreaInsets,
239
248
  dimensions: DisplayMetrics,
240
249
  ) {
241
250
  let left = 0;
@@ -249,8 +258,11 @@ function getLeftPosition(
249
258
  left = triggerPageX + triggerWidth - contentWidth;
250
259
  }
251
260
  return Math.max(
252
- insetLeft,
253
- Math.min(left + alignOffset, dimensions.width - contentWidth - insetRight),
261
+ insets.left,
262
+ Math.min(
263
+ left + alignOffset,
264
+ dimensions.width - contentWidth - insets.right,
265
+ ),
254
266
  );
255
267
  }
256
268
 
@@ -36,6 +36,9 @@ export interface ComponentsConfig {
36
36
  toast?: {
37
37
  icons?: Partial<Record<keyof typeof ToastVariants, IconComponent>>;
38
38
  };
39
+ menu?: {
40
+ selectionIcon?: IconComponent;
41
+ };
39
42
  }
40
43
 
41
44
  const ThemeContext = createContext<ThemeContext | null>(null);