@korsolutions/ui 0.0.58 → 0.0.60

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 (127) hide show
  1. package/dist/module/components/icon/icon.js +3 -3
  2. package/dist/module/components/icon/icon.js.map +1 -1
  3. package/dist/module/components/menu/components/menu-checkbox-item.js +51 -0
  4. package/dist/module/components/menu/components/menu-checkbox-item.js.map +1 -0
  5. package/dist/module/components/menu/components/menu-content.js +2 -2
  6. package/dist/module/components/menu/components/menu-content.js.map +1 -1
  7. package/dist/module/components/menu/components/menu-group.js +18 -0
  8. package/dist/module/components/menu/components/menu-group.js.map +1 -0
  9. package/dist/module/components/menu/components/menu-item.js +14 -6
  10. package/dist/module/components/menu/components/menu-item.js.map +1 -1
  11. package/dist/module/components/menu/components/menu-label.js +17 -0
  12. package/dist/module/components/menu/components/menu-label.js.map +1 -0
  13. package/dist/module/components/menu/components/menu-overlay.js +1 -1
  14. package/dist/module/components/menu/components/menu-overlay.js.map +1 -1
  15. package/dist/module/components/menu/components/menu-radio-group.js +24 -0
  16. package/dist/module/components/menu/components/menu-radio-group.js.map +1 -0
  17. package/dist/module/components/menu/components/menu-radio-item.js +54 -0
  18. package/dist/module/components/menu/components/menu-radio-item.js.map +1 -0
  19. package/dist/module/components/menu/components/menu-selection-indicator.js +29 -0
  20. package/dist/module/components/menu/components/menu-selection-indicator.js.map +1 -0
  21. package/dist/module/components/menu/components/menu-separator.js +17 -0
  22. package/dist/module/components/menu/components/menu-separator.js.map +1 -0
  23. package/dist/module/components/menu/components/menu-shortcut.js +17 -0
  24. package/dist/module/components/menu/components/menu-shortcut.js.map +1 -0
  25. package/dist/module/components/menu/context.js +8 -0
  26. package/dist/module/components/menu/context.js.map +1 -1
  27. package/dist/module/components/menu/index.js +15 -1
  28. package/dist/module/components/menu/index.js.map +1 -1
  29. package/dist/module/components/menu/use-organized-children.js +39 -0
  30. package/dist/module/components/menu/use-organized-children.js.map +1 -0
  31. package/dist/module/components/menu/variants/default.js +73 -6
  32. package/dist/module/components/menu/variants/default.js.map +1 -1
  33. package/dist/module/components/phone-input/components/country-picker.js +1 -1
  34. package/dist/module/components/popover/components/popover-content.js +2 -5
  35. package/dist/module/components/popover/components/popover-content.js.map +1 -1
  36. package/dist/module/components/portal/index.js +1 -0
  37. package/dist/module/components/portal/index.js.map +1 -1
  38. package/dist/module/components/portal/portal-offset.js +32 -0
  39. package/dist/module/components/portal/portal-offset.js.map +1 -0
  40. package/dist/module/components/portal/portal.js +39 -17
  41. package/dist/module/components/portal/portal.js.map +1 -1
  42. package/dist/module/components/select/components/select-content.js +3 -3
  43. package/dist/module/components/select/components/select-content.js.map +1 -1
  44. package/dist/module/components/textarea/variants/default.js +7 -0
  45. package/dist/module/components/textarea/variants/default.js.map +1 -1
  46. package/dist/module/hooks/use-relative-position.js +37 -28
  47. package/dist/module/hooks/use-relative-position.js.map +1 -1
  48. package/dist/module/themes/provider.js.map +1 -1
  49. package/dist/module/utils/element-utils.js +11 -0
  50. package/dist/module/utils/element-utils.js.map +1 -0
  51. package/dist/typescript/src/components/icon/icon.d.ts +2 -2
  52. package/dist/typescript/src/components/icon/icon.d.ts.map +1 -1
  53. package/dist/typescript/src/components/menu/components/menu-checkbox-item.d.ts +13 -0
  54. package/dist/typescript/src/components/menu/components/menu-checkbox-item.d.ts.map +1 -0
  55. package/dist/typescript/src/components/menu/components/menu-content.d.ts.map +1 -1
  56. package/dist/typescript/src/components/menu/components/menu-group.d.ts +9 -0
  57. package/dist/typescript/src/components/menu/components/menu-group.d.ts.map +1 -0
  58. package/dist/typescript/src/components/menu/components/menu-item.d.ts +3 -3
  59. package/dist/typescript/src/components/menu/components/menu-item.d.ts.map +1 -1
  60. package/dist/typescript/src/components/menu/components/menu-label.d.ts +9 -0
  61. package/dist/typescript/src/components/menu/components/menu-label.d.ts.map +1 -0
  62. package/dist/typescript/src/components/menu/components/menu-overlay.d.ts.map +1 -1
  63. package/dist/typescript/src/components/menu/components/menu-radio-group.d.ts +10 -0
  64. package/dist/typescript/src/components/menu/components/menu-radio-group.d.ts.map +1 -0
  65. package/dist/typescript/src/components/menu/components/menu-radio-item.d.ts +12 -0
  66. package/dist/typescript/src/components/menu/components/menu-radio-item.d.ts.map +1 -0
  67. package/dist/typescript/src/components/menu/components/menu-selection-indicator.d.ts +7 -0
  68. package/dist/typescript/src/components/menu/components/menu-selection-indicator.d.ts.map +1 -0
  69. package/dist/typescript/src/components/menu/components/menu-separator.d.ts +8 -0
  70. package/dist/typescript/src/components/menu/components/menu-separator.d.ts.map +1 -0
  71. package/dist/typescript/src/components/menu/components/menu-shortcut.d.ts +9 -0
  72. package/dist/typescript/src/components/menu/components/menu-shortcut.d.ts.map +1 -0
  73. package/dist/typescript/src/components/menu/context.d.ts +7 -1
  74. package/dist/typescript/src/components/menu/context.d.ts.map +1 -1
  75. package/dist/typescript/src/components/menu/index.d.ts +21 -0
  76. package/dist/typescript/src/components/menu/index.d.ts.map +1 -1
  77. package/dist/typescript/src/components/menu/types.d.ts +15 -2
  78. package/dist/typescript/src/components/menu/types.d.ts.map +1 -1
  79. package/dist/typescript/src/components/menu/use-organized-children.d.ts +3 -0
  80. package/dist/typescript/src/components/menu/use-organized-children.d.ts.map +1 -0
  81. package/dist/typescript/src/components/menu/variants/default.d.ts.map +1 -1
  82. package/dist/typescript/src/components/popover/components/popover-content.d.ts.map +1 -1
  83. package/dist/typescript/src/components/portal/index.d.ts +1 -0
  84. package/dist/typescript/src/components/portal/index.d.ts.map +1 -1
  85. package/dist/typescript/src/components/portal/portal-offset.d.ts +13 -0
  86. package/dist/typescript/src/components/portal/portal-offset.d.ts.map +1 -0
  87. package/dist/typescript/src/components/portal/portal.d.ts +3 -2
  88. package/dist/typescript/src/components/portal/portal.d.ts.map +1 -1
  89. package/dist/typescript/src/components/select/components/select-content.d.ts.map +1 -1
  90. package/dist/typescript/src/components/textarea/variants/default.d.ts.map +1 -1
  91. package/dist/typescript/src/hooks/use-relative-position.d.ts +4 -7
  92. package/dist/typescript/src/hooks/use-relative-position.d.ts.map +1 -1
  93. package/dist/typescript/src/themes/provider.d.ts +5 -2
  94. package/dist/typescript/src/themes/provider.d.ts.map +1 -1
  95. package/dist/typescript/src/types/element.types.d.ts +2 -0
  96. package/dist/typescript/src/types/element.types.d.ts.map +1 -1
  97. package/dist/typescript/src/utils/element-utils.d.ts +3 -0
  98. package/dist/typescript/src/utils/element-utils.d.ts.map +1 -0
  99. package/package.json +1 -1
  100. package/src/components/icon/icon.tsx +9 -3
  101. package/src/components/menu/components/menu-checkbox-item.tsx +73 -0
  102. package/src/components/menu/components/menu-content.tsx +3 -2
  103. package/src/components/menu/components/menu-group.tsx +25 -0
  104. package/src/components/menu/components/menu-item.tsx +25 -9
  105. package/src/components/menu/components/menu-label.tsx +21 -0
  106. package/src/components/menu/components/menu-overlay.tsx +11 -2
  107. package/src/components/menu/components/menu-radio-group.tsx +35 -0
  108. package/src/components/menu/components/menu-radio-item.tsx +76 -0
  109. package/src/components/menu/components/menu-selection-indicator.tsx +26 -0
  110. package/src/components/menu/components/menu-separator.tsx +22 -0
  111. package/src/components/menu/components/menu-shortcut.tsx +21 -0
  112. package/src/components/menu/context.ts +18 -1
  113. package/src/components/menu/index.ts +21 -0
  114. package/src/components/menu/types.ts +20 -2
  115. package/src/components/menu/use-organized-children.tsx +38 -0
  116. package/src/components/menu/variants/default.tsx +73 -6
  117. package/src/components/phone-input/components/country-picker.tsx +1 -1
  118. package/src/components/popover/components/popover-content.tsx +1 -4
  119. package/src/components/portal/index.ts +1 -0
  120. package/src/components/portal/portal-offset.ts +28 -0
  121. package/src/components/portal/portal.tsx +77 -27
  122. package/src/components/select/components/select-content.tsx +14 -5
  123. package/src/components/textarea/variants/default.tsx +7 -0
  124. package/src/hooks/use-relative-position.ts +53 -41
  125. package/src/themes/provider.tsx +5 -4
  126. package/src/types/element.types.ts +10 -1
  127. package/src/utils/element-utils.ts +12 -0
@@ -0,0 +1,35 @@
1
+ import React, { useMemo } from "react";
2
+ import { View, type StyleProp, type ViewStyle } from "react-native";
3
+ import { MenuRadioGroupContext, useMenu } from "../context";
4
+
5
+ export interface MenuRadioGroupProps {
6
+ children?: React.ReactNode;
7
+ value: string;
8
+ onValueChange: (value: string) => void;
9
+ style?: StyleProp<ViewStyle>;
10
+ }
11
+
12
+ export function MenuRadioGroup(props: MenuRadioGroupProps) {
13
+ const menu = useMenu();
14
+ const composedStyle = [menu.styles?.radioGroup, props.style];
15
+
16
+ const contextValue = useMemo(
17
+ () => ({
18
+ value: props.value,
19
+ onValueChange: props.onValueChange,
20
+ }),
21
+ [props.value, props.onValueChange],
22
+ );
23
+
24
+ return (
25
+ <MenuRadioGroupContext.Provider value={contextValue}>
26
+ <View
27
+ accessibilityRole="radiogroup"
28
+ role="radiogroup"
29
+ style={composedStyle}
30
+ >
31
+ {props.children}
32
+ </View>
33
+ </MenuRadioGroupContext.Provider>
34
+ );
35
+ }
@@ -0,0 +1,76 @@
1
+ import React, { useState } from "react";
2
+ import { Pressable, type StyleProp, type ViewStyle } from "react-native";
3
+ import { useMenu, useMenuRadioGroup } from "../context";
4
+ import type { MenuRadioItemState } from "../types";
5
+ import { useOrganizedChildren } from "../use-organized-children";
6
+ import { MenuSelectionIndicator } from "./menu-selection-indicator";
7
+
8
+ export interface MenuRadioItemProps {
9
+ children: React.ReactNode;
10
+ value: string;
11
+ disabled?: boolean;
12
+ closeOnPress?: boolean;
13
+ render?: (props: MenuRadioItemProps) => React.ReactNode;
14
+ style?: StyleProp<ViewStyle>;
15
+ }
16
+
17
+ const calculateState = (
18
+ isHovered: boolean,
19
+ isSelected: boolean,
20
+ disabled?: boolean,
21
+ ): MenuRadioItemState => {
22
+ if (disabled) return "disabled";
23
+ if (isSelected) return "selected";
24
+ if (isHovered) return "hovered";
25
+ return "default";
26
+ };
27
+
28
+ export function MenuRadioItem(props: MenuRadioItemProps) {
29
+ const menu = useMenu();
30
+ const radioGroup = useMenuRadioGroup();
31
+ const [isHovered, setIsHovered] = useState(false);
32
+ const isSelected = radioGroup.value === props.value;
33
+ const state = calculateState(isHovered, isSelected, props.disabled);
34
+
35
+ const composedStyle = [
36
+ menu.styles?.radioItem?.default,
37
+ menu.styles?.radioItem?.[state],
38
+ props.style,
39
+ ];
40
+
41
+ const handlePress = () => {
42
+ if (props.disabled) return;
43
+ radioGroup.onValueChange(props.value);
44
+ if (props.closeOnPress) {
45
+ menu.setIsOpen(false);
46
+ }
47
+ };
48
+
49
+ const organizedChildren = useOrganizedChildren(props.children);
50
+
51
+ if (props.render) {
52
+ return (
53
+ <>
54
+ {props.render({
55
+ ...props,
56
+ children: organizedChildren,
57
+ })}
58
+ </>
59
+ );
60
+ }
61
+
62
+ return (
63
+ <Pressable
64
+ onPress={handlePress}
65
+ onPointerEnter={() => setIsHovered(true)}
66
+ onPointerLeave={() => setIsHovered(false)}
67
+ disabled={props.disabled}
68
+ accessibilityRole="radio"
69
+ accessibilityState={{ checked: isSelected, disabled: props.disabled }}
70
+ style={composedStyle}
71
+ >
72
+ {organizedChildren}
73
+ <MenuSelectionIndicator isSelected={isSelected} />
74
+ </Pressable>
75
+ );
76
+ }
@@ -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
+ }
@@ -0,0 +1,22 @@
1
+ import React from "react";
2
+ import { View, type StyleProp, type ViewStyle } from "react-native";
3
+ import { useMenu } from "../context";
4
+
5
+ export interface MenuSeparatorProps {
6
+ render?: (props: MenuSeparatorProps) => React.ReactNode;
7
+ style?: StyleProp<ViewStyle>;
8
+ }
9
+
10
+ export function MenuSeparator(props: MenuSeparatorProps) {
11
+ const menu = useMenu();
12
+ const composedStyle = [menu.styles?.separator, props.style];
13
+
14
+ const Component = props.render ?? View;
15
+ return (
16
+ <Component
17
+ {...props}
18
+ role="separator"
19
+ style={composedStyle}
20
+ />
21
+ );
22
+ }
@@ -0,0 +1,21 @@
1
+ import React from "react";
2
+ import { Text, type StyleProp, type TextStyle } from "react-native";
3
+ import { useMenu } from "../context";
4
+
5
+ export interface MenuShortcutProps {
6
+ children: string;
7
+ render?: (props: MenuShortcutProps) => React.ReactNode;
8
+ style?: StyleProp<TextStyle>;
9
+ }
10
+
11
+ export function MenuShortcut(props: MenuShortcutProps) {
12
+ const menu = useMenu();
13
+ const composedStyle = [menu.styles?.shortcut, props.style];
14
+
15
+ const Component = props.render ?? Text;
16
+ return (
17
+ <Component {...props} style={composedStyle}>
18
+ {props.children}
19
+ </Component>
20
+ );
21
+ }
@@ -1,6 +1,6 @@
1
- import type { LayoutPosition } from "../../hooks/use-relative-position";
2
1
  import { createContext, type Dispatch, useContext } from "react";
3
2
  import type { LayoutRectangle } from "react-native";
3
+ import type { LayoutPosition } from "../../hooks/use-relative-position";
4
4
  import type { MenuStyles } from "./types";
5
5
 
6
6
  export interface MenuContext {
@@ -23,3 +23,20 @@ export const useMenu = () => {
23
23
  }
24
24
  return context;
25
25
  };
26
+
27
+ export interface MenuRadioGroupContextValue {
28
+ value: string;
29
+ onValueChange: (value: string) => void;
30
+ }
31
+
32
+ export const MenuRadioGroupContext = createContext<
33
+ MenuRadioGroupContextValue | undefined
34
+ >(undefined);
35
+
36
+ export const useMenuRadioGroup = () => {
37
+ const context = useContext(MenuRadioGroupContext);
38
+ if (!context) {
39
+ throw new Error("useMenuRadioGroup must be used within a Menu.RadioGroup");
40
+ }
41
+ return context;
42
+ };
@@ -1,8 +1,15 @@
1
+ import { MenuCheckboxItem } from "./components/menu-checkbox-item";
1
2
  import { MenuContent } from "./components/menu-content";
3
+ import { MenuGroup } from "./components/menu-group";
2
4
  import { MenuItem } from "./components/menu-item";
5
+ import { MenuLabel } from "./components/menu-label";
3
6
  import { MenuOverlay } from "./components/menu-overlay";
4
7
  import { MenuPortal } from "./components/menu-portal";
8
+ import { MenuRadioGroup } from "./components/menu-radio-group";
9
+ import { MenuRadioItem } from "./components/menu-radio-item";
5
10
  import { MenuRoot } from "./components/menu-root";
11
+ import { MenuSeparator } from "./components/menu-separator";
12
+ import { MenuShortcut } from "./components/menu-shortcut";
6
13
  import { MenuTrigger } from "./components/menu-trigger";
7
14
 
8
15
  export const Menu = {
@@ -12,12 +19,26 @@ export const Menu = {
12
19
  Overlay: MenuOverlay,
13
20
  Content: MenuContent,
14
21
  Item: MenuItem,
22
+ Group: MenuGroup,
23
+ Label: MenuLabel,
24
+ Separator: MenuSeparator,
25
+ CheckboxItem: MenuCheckboxItem,
26
+ RadioGroup: MenuRadioGroup,
27
+ RadioItem: MenuRadioItem,
28
+ Shortcut: MenuShortcut,
15
29
  };
16
30
 
31
+ export type { MenuCheckboxItemProps } from "./components/menu-checkbox-item";
17
32
  export type { MenuContentProps } from "./components/menu-content";
33
+ export type { MenuGroupProps } from "./components/menu-group";
18
34
  export type { MenuItemProps } from "./components/menu-item";
35
+ export type { MenuLabelProps } from "./components/menu-label";
19
36
  export type { MenuOverlayProps } from "./components/menu-overlay";
20
37
  export type { MenuPortalProps } from "./components/menu-portal";
38
+ export type { MenuRadioGroupProps } from "./components/menu-radio-group";
39
+ export type { MenuRadioItemProps } from "./components/menu-radio-item";
21
40
  export type { MenuRootProps } from "./components/menu-root";
41
+ export type { MenuSeparatorProps } from "./components/menu-separator";
42
+ export type { MenuShortcutProps } from "./components/menu-shortcut";
22
43
  export type { MenuTriggerProps } from "./components/menu-trigger";
23
44
  export type { MenuStyles } from "./types";
@@ -1,11 +1,29 @@
1
+ import type { StyleProp, TextStyle, ViewStyle } from "react-native";
2
+ import type { SvgProps } from "../../types/props.types";
1
3
  import type { MenuContentProps } from "./components/menu-content";
2
- import type { MenuItemProps } from "./components/menu-item";
3
4
  import type { MenuOverlayProps } from "./components/menu-overlay";
4
5
 
5
6
  export type MenuButtonState = "default" | "hovered";
7
+ export type MenuCheckboxItemState = "default" | "hovered" | "disabled";
8
+ export type MenuRadioItemState =
9
+ | "default"
10
+ | "hovered"
11
+ | "selected"
12
+ | "disabled";
6
13
 
7
14
  export interface MenuStyles {
8
15
  content?: MenuContentProps["style"];
9
- item?: Partial<Record<MenuButtonState, MenuItemProps["style"]>>;
16
+ item?: Partial<Record<MenuButtonState, StyleProp<ViewStyle>>>;
17
+ itemText?: StyleProp<TextStyle>;
18
+ itemIcon?: SvgProps;
10
19
  overlay?: MenuOverlayProps["style"];
20
+
21
+ group?: StyleProp<ViewStyle>;
22
+ label?: StyleProp<TextStyle>;
23
+ separator?: StyleProp<ViewStyle>;
24
+ checkboxItem?: Partial<Record<MenuCheckboxItemState, StyleProp<ViewStyle>>>;
25
+ selectionIndicator?: TextStyle & SvgProps;
26
+ radioGroup?: StyleProp<ViewStyle>;
27
+ radioItem?: Partial<Record<MenuRadioItemState, StyleProp<ViewStyle>>>;
28
+ shortcut?: StyleProp<TextStyle>;
11
29
  }
@@ -0,0 +1,38 @@
1
+ import React, { useMemo } from "react";
2
+ import { Text } from "react-native";
3
+ import { getElementProp } from "../../utils/element-utils";
4
+ import { Icon } from "../icon";
5
+ import { useMenu } from "./context";
6
+
7
+ export function useOrganizedChildren(children: React.ReactNode) {
8
+ const menu = useMenu();
9
+
10
+ const organizedChildren = useMemo(() => {
11
+ if (typeof children === "string") {
12
+ return <Text style={menu.styles?.itemText}>{children}</Text>;
13
+ }
14
+ if (Array.isArray(children)) {
15
+ return children.map((child, index) => {
16
+ if (typeof child === "string") {
17
+ return (
18
+ <Text key={index} style={menu.styles?.itemText}>
19
+ {child}
20
+ </Text>
21
+ );
22
+ } else if (React.isValidElement(child) && child.type === Icon) {
23
+ return React.cloneElement(child as React.ReactElement<any>, {
24
+ key: child.key,
25
+ ...menu.styles?.itemIcon,
26
+ style: [
27
+ menu.styles?.itemIcon?.style,
28
+ getElementProp(child, "style"),
29
+ ],
30
+ });
31
+ }
32
+ return child;
33
+ });
34
+ }
35
+ return children;
36
+ }, [children, menu.styles?.itemIcon]);
37
+ return organizedChildren;
38
+ }
@@ -4,9 +4,7 @@ import { useThemedStyles } from "../../../utils/use-themed-styles";
4
4
  export const useMenuVariantDefault = (): MenuStyles => {
5
5
  return useThemedStyles(
6
6
  ({ colors, radius, fontFamily, fontSize }): MenuStyles => ({
7
- overlay: {
8
- backgroundColor: "rgba(0, 0, 0, 0.5)",
9
- },
7
+ overlay: {},
10
8
  content: {
11
9
  overflow: "hidden",
12
10
  backgroundColor: colors.surface,
@@ -21,15 +19,84 @@ export const useMenuVariantDefault = (): MenuStyles => {
21
19
  },
22
20
  item: {
23
21
  default: {
22
+ flexDirection: "row",
23
+ alignItems: "center",
24
+ paddingVertical: 12,
25
+ paddingHorizontal: 16,
26
+ gap: 8,
27
+ },
28
+ hovered: {
29
+ backgroundColor: colors.muted,
30
+ },
31
+ },
32
+ itemText: {
33
+ fontFamily: fontFamily,
34
+ fontSize: fontSize,
35
+ color: colors.foreground,
36
+ },
37
+ itemIcon: {
38
+ color: colors.foreground,
39
+ size: fontSize * 1.25,
40
+ },
41
+ label: {
42
+ paddingVertical: 8,
43
+ paddingHorizontal: 16,
44
+ fontFamily: fontFamily,
45
+ fontSize: fontSize * 0.75,
46
+ fontWeight: "600",
47
+ color: colors.mutedForeground,
48
+ },
49
+ separator: {
50
+ height: 1,
51
+ backgroundColor: colors.border,
52
+ marginVertical: 4,
53
+ marginHorizontal: 8,
54
+ },
55
+ checkboxItem: {
56
+ default: {
57
+ flexDirection: "row",
58
+ alignItems: "center",
24
59
  paddingVertical: 12,
25
60
  paddingHorizontal: 16,
26
- fontFamily: fontFamily,
27
- fontSize: fontSize,
28
- color: colors.foreground,
61
+ gap: 8,
29
62
  },
30
63
  hovered: {
31
64
  backgroundColor: colors.muted,
32
65
  },
66
+ disabled: {
67
+ opacity: 0.5,
68
+ },
69
+ },
70
+ selectionIndicator: {
71
+ color: colors.foreground,
72
+ fontSize: fontSize,
73
+ width: fontSize,
74
+ size: fontSize,
75
+ strokeWidth: 2,
76
+ marginLeft: "auto",
77
+ style: {
78
+ marginLeft: "auto",
79
+ },
80
+ },
81
+ radioItem: {
82
+ default: {
83
+ flexDirection: "row",
84
+ alignItems: "center",
85
+ paddingVertical: 12,
86
+ paddingHorizontal: 16,
87
+ gap: 8,
88
+ },
89
+ hovered: {
90
+ backgroundColor: colors.muted,
91
+ },
92
+ disabled: {
93
+ opacity: 0.5,
94
+ },
95
+ },
96
+ shortcut: {
97
+ fontSize: fontSize * 0.75,
98
+ fontFamily: fontFamily,
99
+ color: colors.mutedForeground,
33
100
  },
34
101
  }),
35
102
  );
@@ -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,6 +1,18 @@
1
- import { useEffect, useSyncExternalStore } from "react";
2
- import { Platform, View } from "react-native";
3
- import { DEFAULT_PORTAL_HOST, type PortalHostProps, type PortalProps } from "./portal.constants";
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";
11
+ import {
12
+ DEFAULT_PORTAL_HOST,
13
+ type PortalHostProps,
14
+ type PortalProps,
15
+ } from "./portal.constants";
4
16
 
5
17
  type PortalMap = Map<string, React.ReactNode>;
6
18
  type PortalHostMap = Map<string, PortalMap>;
@@ -11,7 +23,10 @@ type PortalStore = {
11
23
  };
12
24
 
13
25
  const store: PortalStore = {
14
- map: new Map<string, PortalMap>().set(DEFAULT_PORTAL_HOST, new Map<string, React.ReactNode>()),
26
+ map: new Map<string, PortalMap>().set(
27
+ DEFAULT_PORTAL_HOST,
28
+ new Map<string, React.ReactNode>(),
29
+ ),
15
30
  listeners: new Set(),
16
31
  };
17
32
 
@@ -30,7 +45,11 @@ function subscribe(cb: () => void) {
30
45
  };
31
46
  }
32
47
 
33
- function updatePortal(hostName: string, name: string, children: React.ReactNode) {
48
+ function updatePortal(
49
+ hostName: string,
50
+ name: string,
51
+ children: React.ReactNode,
52
+ ) {
34
53
  const next = new Map(store.map);
35
54
  const portal = next.get(hostName) ?? new Map<string, React.ReactNode>();
36
55
  portal.set(name, children);
@@ -48,34 +67,65 @@ function removePortal(hostName: string, name: string) {
48
67
  emit();
49
68
  }
50
69
 
51
- export function PortalHost({ name = DEFAULT_PORTAL_HOST, container }: PortalHostProps) {
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
+
105
+ export function PortalHost({
106
+ name = DEFAULT_PORTAL_HOST,
107
+ container,
108
+ }: PortalHostProps) {
52
109
  const map = useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
53
110
  const portalMap = map.get(name) ?? new Map<string, React.ReactNode>();
54
- if (portalMap.size === 0) return null;
55
-
56
- const Container = Platform.select({
57
- default: (props: React.PropsWithChildren) => (
58
- <View
59
- {...props}
60
- style={{
61
- position: "absolute",
62
- top: 0,
63
- left: 0,
64
- right: 0,
65
- bottom: 0,
66
- elevation: 999,
67
- zIndex: 999,
68
- pointerEvents: "box-none",
69
- }}
70
- />
71
- ),
72
- ...container,
73
- });
111
+ const Container = useMemo(
112
+ () =>
113
+ Platform.select({
114
+ default: DefaultContainer,
115
+ ...container,
116
+ }),
117
+ [container],
118
+ );
74
119
 
120
+ if (portalMap.size === 0) return null;
75
121
  return <Container>{Array.from(portalMap.values())}</Container>;
76
122
  }
77
123
 
78
- export function Portal({ name, hostName = DEFAULT_PORTAL_HOST, children }: PortalProps) {
124
+ export function Portal({
125
+ name,
126
+ hostName = DEFAULT_PORTAL_HOST,
127
+ children,
128
+ }: PortalProps) {
79
129
  useEffect(() => {
80
130
  updatePortal(hostName, name, children);
81
131
  }, [hostName, name, children]);
@@ -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
  }}