@korsolutions/ui 0.0.38 → 0.0.40

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/autocomplete/autocomplete.js +31 -0
  2. package/dist/module/components/autocomplete/autocomplete.js.map +1 -0
  3. package/dist/module/components/autocomplete/variants/default.js +88 -0
  4. package/dist/module/components/autocomplete/variants/default.js.map +1 -0
  5. package/dist/module/components/autocomplete/variants/index.js +7 -0
  6. package/dist/module/components/autocomplete/variants/index.js.map +1 -0
  7. package/dist/module/components/index.js +1 -0
  8. package/dist/module/components/index.js.map +1 -1
  9. package/dist/module/components/input/numeric-input.js +7 -6
  10. package/dist/module/components/input/numeric-input.js.map +1 -1
  11. package/dist/module/primitives/alert-dialog/alert-dialog-trigger.js.map +1 -1
  12. package/dist/module/primitives/autocomplete/autocomplete-content.js +33 -0
  13. package/dist/module/primitives/autocomplete/autocomplete-content.js.map +1 -0
  14. package/dist/module/primitives/autocomplete/autocomplete-empty.js +17 -0
  15. package/dist/module/primitives/autocomplete/autocomplete-empty.js.map +1 -0
  16. package/dist/module/primitives/autocomplete/autocomplete-input.js +72 -0
  17. package/dist/module/primitives/autocomplete/autocomplete-input.js.map +1 -0
  18. package/dist/module/primitives/autocomplete/autocomplete-option.js +54 -0
  19. package/dist/module/primitives/autocomplete/autocomplete-option.js.map +1 -0
  20. package/dist/module/primitives/autocomplete/autocomplete-overlay.js +20 -0
  21. package/dist/module/primitives/autocomplete/autocomplete-overlay.js.map +1 -0
  22. package/dist/module/primitives/autocomplete/autocomplete-portal.js +25 -0
  23. package/dist/module/primitives/autocomplete/autocomplete-portal.js.map +1 -0
  24. package/dist/module/primitives/autocomplete/autocomplete-root.js +69 -0
  25. package/dist/module/primitives/autocomplete/autocomplete-root.js.map +1 -0
  26. package/dist/module/primitives/autocomplete/context.js +12 -0
  27. package/dist/module/primitives/autocomplete/context.js.map +1 -0
  28. package/dist/module/primitives/autocomplete/index.js +20 -0
  29. package/dist/module/primitives/autocomplete/index.js.map +1 -0
  30. package/dist/module/primitives/autocomplete/types.js +4 -0
  31. package/dist/module/primitives/autocomplete/types.js.map +1 -0
  32. package/dist/module/primitives/dropdown-menu/dropdown-menu-divider.js +2 -2
  33. package/dist/module/primitives/dropdown-menu/dropdown-menu-divider.js.map +1 -1
  34. package/dist/module/primitives/dropdown-menu/dropdown-menu-trigger.js +3 -7
  35. package/dist/module/primitives/dropdown-menu/dropdown-menu-trigger.js.map +1 -1
  36. package/dist/module/primitives/index.js +1 -0
  37. package/dist/module/primitives/index.js.map +1 -1
  38. package/dist/module/primitives/input/input.js +3 -4
  39. package/dist/module/primitives/input/input.js.map +1 -1
  40. package/dist/module/primitives/popover/popover-trigger.js +3 -7
  41. package/dist/module/primitives/popover/popover-trigger.js.map +1 -1
  42. package/dist/module/primitives/portal/portal.js +4 -35
  43. package/dist/module/primitives/portal/portal.js.map +1 -1
  44. package/dist/module/primitives/select/context.js.map +1 -1
  45. package/dist/module/primitives/select/select-content.js +16 -5
  46. package/dist/module/primitives/select/select-content.js.map +1 -1
  47. package/dist/module/primitives/select/select-root.js +7 -3
  48. package/dist/module/primitives/select/select-root.js.map +1 -1
  49. package/dist/module/primitives/select/select-trigger.js +16 -11
  50. package/dist/module/primitives/select/select-trigger.js.map +1 -1
  51. package/dist/module/utils/input-utils.js +13 -0
  52. package/dist/module/utils/input-utils.js.map +1 -0
  53. package/dist/module/utils/normalize-layout.js +17 -0
  54. package/dist/module/utils/normalize-layout.js.map +1 -1
  55. package/dist/typescript/src/components/autocomplete/autocomplete.d.ts +11 -0
  56. package/dist/typescript/src/components/autocomplete/autocomplete.d.ts.map +1 -0
  57. package/dist/typescript/src/components/autocomplete/variants/default.d.ts +3 -0
  58. package/dist/typescript/src/components/autocomplete/variants/default.d.ts.map +1 -0
  59. package/dist/typescript/src/components/autocomplete/variants/index.d.ts +5 -0
  60. package/dist/typescript/src/components/autocomplete/variants/index.d.ts.map +1 -0
  61. package/dist/typescript/src/components/index.d.ts +1 -0
  62. package/dist/typescript/src/components/index.d.ts.map +1 -1
  63. package/dist/typescript/src/components/input/numeric-input.d.ts +2 -1
  64. package/dist/typescript/src/components/input/numeric-input.d.ts.map +1 -1
  65. package/dist/typescript/src/primitives/alert-dialog/alert-dialog-trigger.d.ts +1 -147
  66. package/dist/typescript/src/primitives/alert-dialog/alert-dialog-trigger.d.ts.map +1 -1
  67. package/dist/typescript/src/primitives/autocomplete/autocomplete-content.d.ts +8 -0
  68. package/dist/typescript/src/primitives/autocomplete/autocomplete-content.d.ts.map +1 -0
  69. package/dist/typescript/src/primitives/autocomplete/autocomplete-empty.d.ts +9 -0
  70. package/dist/typescript/src/primitives/autocomplete/autocomplete-empty.d.ts.map +1 -0
  71. package/dist/typescript/src/primitives/autocomplete/autocomplete-input.d.ts +7 -0
  72. package/dist/typescript/src/primitives/autocomplete/autocomplete-input.d.ts.map +1 -0
  73. package/dist/typescript/src/primitives/autocomplete/autocomplete-option.d.ts +11 -0
  74. package/dist/typescript/src/primitives/autocomplete/autocomplete-option.d.ts.map +1 -0
  75. package/dist/typescript/src/primitives/autocomplete/autocomplete-overlay.d.ts +10 -0
  76. package/dist/typescript/src/primitives/autocomplete/autocomplete-overlay.d.ts.map +1 -0
  77. package/dist/typescript/src/primitives/autocomplete/autocomplete-portal.d.ts +6 -0
  78. package/dist/typescript/src/primitives/autocomplete/autocomplete-portal.d.ts.map +1 -0
  79. package/dist/typescript/src/primitives/autocomplete/autocomplete-root.d.ts +24 -0
  80. package/dist/typescript/src/primitives/autocomplete/autocomplete-root.d.ts.map +1 -0
  81. package/dist/typescript/src/primitives/autocomplete/context.d.ts +30 -0
  82. package/dist/typescript/src/primitives/autocomplete/context.d.ts.map +1 -0
  83. package/dist/typescript/src/primitives/autocomplete/index.d.ts +25 -0
  84. package/dist/typescript/src/primitives/autocomplete/index.d.ts.map +1 -0
  85. package/dist/typescript/src/primitives/autocomplete/types.d.ts +21 -0
  86. package/dist/typescript/src/primitives/autocomplete/types.d.ts.map +1 -0
  87. package/dist/typescript/src/primitives/dropdown-menu/dropdown-menu-divider.d.ts.map +1 -1
  88. package/dist/typescript/src/primitives/dropdown-menu/dropdown-menu-trigger.d.ts.map +1 -1
  89. package/dist/typescript/src/primitives/index.d.ts +1 -0
  90. package/dist/typescript/src/primitives/index.d.ts.map +1 -1
  91. package/dist/typescript/src/primitives/input/input.d.ts +1 -2
  92. package/dist/typescript/src/primitives/input/input.d.ts.map +1 -1
  93. package/dist/typescript/src/primitives/popover/popover-trigger.d.ts.map +1 -1
  94. package/dist/typescript/src/primitives/portal/portal.constants.d.ts +6 -2
  95. package/dist/typescript/src/primitives/portal/portal.constants.d.ts.map +1 -1
  96. package/dist/typescript/src/primitives/portal/portal.d.ts +2 -5
  97. package/dist/typescript/src/primitives/portal/portal.d.ts.map +1 -1
  98. package/dist/typescript/src/primitives/select/context.d.ts +5 -2
  99. package/dist/typescript/src/primitives/select/context.d.ts.map +1 -1
  100. package/dist/typescript/src/primitives/select/select-content.d.ts.map +1 -1
  101. package/dist/typescript/src/primitives/select/select-root.d.ts.map +1 -1
  102. package/dist/typescript/src/primitives/select/select-trigger.d.ts +0 -5
  103. package/dist/typescript/src/primitives/select/select-trigger.d.ts.map +1 -1
  104. package/dist/typescript/src/types/element.types.d.ts +10 -3
  105. package/dist/typescript/src/types/element.types.d.ts.map +1 -1
  106. package/dist/typescript/src/utils/input-utils.d.ts +3 -0
  107. package/dist/typescript/src/utils/input-utils.d.ts.map +1 -0
  108. package/dist/typescript/src/utils/normalize-layout.d.ts +3 -1
  109. package/dist/typescript/src/utils/normalize-layout.d.ts.map +1 -1
  110. package/package.json +8 -5
  111. package/scripts/build.sh +0 -0
  112. package/src/components/autocomplete/autocomplete.tsx +34 -0
  113. package/src/components/autocomplete/variants/default.tsx +84 -0
  114. package/src/components/autocomplete/variants/index.ts +5 -0
  115. package/src/components/index.ts +1 -0
  116. package/src/components/input/numeric-input.tsx +8 -6
  117. package/src/primitives/alert-dialog/alert-dialog-trigger.tsx +1 -1
  118. package/src/primitives/autocomplete/autocomplete-content.tsx +38 -0
  119. package/src/primitives/autocomplete/autocomplete-empty.tsx +19 -0
  120. package/src/primitives/autocomplete/autocomplete-input.tsx +86 -0
  121. package/src/primitives/autocomplete/autocomplete-option.tsx +66 -0
  122. package/src/primitives/autocomplete/autocomplete-overlay.tsx +29 -0
  123. package/src/primitives/autocomplete/autocomplete-portal.tsx +27 -0
  124. package/src/primitives/autocomplete/autocomplete-root.tsx +111 -0
  125. package/src/primitives/autocomplete/context.ts +45 -0
  126. package/src/primitives/autocomplete/index.ts +27 -0
  127. package/src/primitives/autocomplete/types.ts +23 -0
  128. package/src/primitives/dropdown-menu/dropdown-menu-divider.tsx +2 -2
  129. package/src/primitives/dropdown-menu/dropdown-menu-trigger.tsx +3 -7
  130. package/src/primitives/index.ts +1 -0
  131. package/src/primitives/input/input.tsx +3 -6
  132. package/src/primitives/popover/popover-trigger.tsx +3 -7
  133. package/src/primitives/portal/portal.constants.tsx +2 -2
  134. package/src/primitives/portal/portal.tsx +4 -36
  135. package/src/primitives/select/context.ts +5 -2
  136. package/src/primitives/select/select-content.tsx +16 -9
  137. package/src/primitives/select/select-root.tsx +7 -3
  138. package/src/primitives/select/select-trigger.tsx +19 -21
  139. package/src/types/element.types.ts +10 -3
  140. package/src/utils/input-utils.ts +10 -0
  141. package/src/utils/normalize-layout.ts +22 -1
@@ -0,0 +1,45 @@
1
+ import type { LayoutPosition } from "@/hooks";
2
+ import { createContext, type Dispatch, useContext } from "react";
3
+ import type { LayoutRectangle } from "react-native";
4
+ import type { AutocompleteOption, AutocompleteState, AutocompleteStyles } from "./types";
5
+
6
+ export interface AutocompleteContext {
7
+ value?: string;
8
+ onChange?: (value: string) => void;
9
+ placeholder?: string;
10
+
11
+ inputValue?: string;
12
+ setInputValue?: (value: string) => void;
13
+
14
+ isOpen: boolean;
15
+ setIsOpen: Dispatch<React.SetStateAction<boolean>>;
16
+ inputPosition: LayoutPosition;
17
+ setInputPosition: Dispatch<React.SetStateAction<LayoutPosition>>;
18
+ contentLayout: LayoutRectangle;
19
+ setContentLayout: Dispatch<React.SetStateAction<LayoutRectangle>>;
20
+
21
+ options: Array<AutocompleteOption>;
22
+ setOptions: Dispatch<React.SetStateAction<Array<AutocompleteOption>>>;
23
+
24
+ openOnFocus: boolean;
25
+
26
+ blurInput: () => void;
27
+ setBlurInput: Dispatch<React.SetStateAction<() => void>>;
28
+ setInputDisplayValueSetter: Dispatch<React.SetStateAction<(value: string) => void>>;
29
+ setInputDisplayValue: (value: string) => void;
30
+
31
+ isDisabled: boolean;
32
+
33
+ state: AutocompleteState;
34
+ styles: AutocompleteStyles | null;
35
+ }
36
+
37
+ export const AutocompleteContext = createContext<AutocompleteContext | undefined>(undefined);
38
+
39
+ export const useAutocomplete = () => {
40
+ const context = useContext(AutocompleteContext);
41
+ if (!context) {
42
+ throw new Error("useAutocomplete must be used within an AutocompleteProvider");
43
+ }
44
+ return context;
45
+ };
@@ -0,0 +1,27 @@
1
+ export type { AutocompleteContentProps } from "./autocomplete-content";
2
+ export type { AutocompleteEmptyProps } from "./autocomplete-empty";
3
+ export type { AutocompleteInputProps } from "./autocomplete-input";
4
+ export type { AutocompleteOptionProps } from "./autocomplete-option";
5
+ export type { AutocompleteOverlayProps } from "./autocomplete-overlay";
6
+ export type { AutocompletePortalProps } from "./autocomplete-portal";
7
+ export type { AutocompleteRootBaseProps, AutocompleteRootProps } from "./autocomplete-root";
8
+ export { useAutocomplete } from "./context";
9
+ export type { AutocompleteOption, AutocompleteOptionState, AutocompleteState, AutocompleteStyles } from "./types";
10
+
11
+ import { AutocompleteContent } from "./autocomplete-content";
12
+ import { AutocompleteEmpty } from "./autocomplete-empty";
13
+ import { AutocompleteInput } from "./autocomplete-input";
14
+ import { AutocompleteOption } from "./autocomplete-option";
15
+ import { AutocompleteOverlay } from "./autocomplete-overlay";
16
+ import { AutocompletePortal } from "./autocomplete-portal";
17
+ import { AutocompleteRoot } from "./autocomplete-root";
18
+
19
+ export const AutocompletePrimitive = {
20
+ Root: AutocompleteRoot,
21
+ Input: AutocompleteInput,
22
+ Portal: AutocompletePortal,
23
+ Overlay: AutocompleteOverlay,
24
+ Content: AutocompleteContent,
25
+ Option: AutocompleteOption,
26
+ Empty: AutocompleteEmpty,
27
+ };
@@ -0,0 +1,23 @@
1
+ import type { AutocompleteContentProps } from "./autocomplete-content";
2
+ import type { AutocompleteEmptyProps } from "./autocomplete-empty";
3
+ import type { AutocompleteInputProps } from "./autocomplete-input";
4
+ import type { AutocompleteOptionProps } from "./autocomplete-option";
5
+ import type { AutocompleteOverlayProps } from "./autocomplete-overlay";
6
+ import type { AutocompleteRootProps } from "./autocomplete-root";
7
+
8
+ export type AutocompleteState = "default" | "focused" | "disabled";
9
+ export type AutocompleteOptionState = AutocompleteState | "hovered" | "selected";
10
+
11
+ export interface AutocompleteStyles {
12
+ root?: Partial<Record<AutocompleteState, AutocompleteRootProps["style"]>>;
13
+ input?: Partial<Record<AutocompleteState, AutocompleteInputProps["style"]>>;
14
+ overlay?: Partial<Record<AutocompleteState, AutocompleteOverlayProps["style"]>>;
15
+ content?: Partial<Record<AutocompleteState, AutocompleteContentProps["style"]>>;
16
+ option?: Partial<Record<AutocompleteOptionState, AutocompleteOptionProps["style"]>>;
17
+ empty?: Partial<Record<AutocompleteState, AutocompleteEmptyProps["style"]>>;
18
+ }
19
+
20
+ export interface AutocompleteOption {
21
+ value: string;
22
+ label: string;
23
+ }
@@ -1,5 +1,5 @@
1
1
  import React from "react";
2
- import { type StyleProp, View, type ViewStyle } from "react-native";
2
+ import { type StyleProp, StyleSheet, View, type ViewStyle } from "react-native";
3
3
  import { useDropdownMenu } from "./context";
4
4
 
5
5
  export interface DropdownMenuDividerProps {
@@ -11,7 +11,7 @@ export interface DropdownMenuDividerProps {
11
11
  export function DropdownMenuDivider(props: DropdownMenuDividerProps) {
12
12
  const menu = useDropdownMenu();
13
13
 
14
- const composedStyle = [menu.styles?.divider, props.style];
14
+ const composedStyle = StyleSheet.flatten([menu.styles?.divider, props.style]);
15
15
 
16
16
  const Component = props.render ?? View;
17
17
  return <Component {...props} style={composedStyle} />;
@@ -1,4 +1,5 @@
1
1
  import type { ViewRef } from "@/types/element.types";
2
+ import { measureLayoutPosition } from "@/utils/normalize-layout";
2
3
  import React, { forwardRef, useImperativeHandle, useRef } from "react";
3
4
  import { type PressableProps } from "react-native";
4
5
  import { useDropdownMenu } from "./context";
@@ -18,13 +19,8 @@ export const DropdownMenuTrigger = forwardRef<DropdownMenuTriggerRef, DropdownMe
18
19
 
19
20
  const onTriggerPress = async () => {
20
21
  if (!dropdownMenu.isOpen) {
21
- triggerRef.current?.measureInWindow((pageX, pageY, width, height) => {
22
- dropdownMenu.setTriggerPosition({
23
- height,
24
- width,
25
- pageX,
26
- pageY,
27
- });
22
+ measureLayoutPosition(triggerRef.current, (layout) => {
23
+ dropdownMenu.setTriggerPosition(layout);
28
24
  dropdownMenu.setIsOpen(true);
29
25
  });
30
26
  } else {
@@ -2,6 +2,7 @@ export * from "./field";
2
2
  export * from "./input";
3
3
  export * from "./button";
4
4
  export * from "./select";
5
+ export * from "./autocomplete";
5
6
  export * from "./card";
6
7
  export * from "./empty";
7
8
  export * from "./avatar";
@@ -1,6 +1,6 @@
1
1
  import type { TextInputRef } from "@/types/element.types";
2
2
  import { forwardRef, useState } from "react";
3
- import { TextInput, type TextInputProps } from "react-native";
3
+ import { StyleSheet, TextInput, type TextInputProps } from "react-native";
4
4
  import { useFieldOptional } from "../field/context";
5
5
  import type { InputState, InputStyles } from "./types";
6
6
 
@@ -11,8 +11,6 @@ export type InputPrimitiveBaseProps = Omit<TextInputProps, "onChange"> & {
11
11
  };
12
12
 
13
13
  export interface InputPrimitiveProps extends InputPrimitiveBaseProps {
14
- render?: (props: InputPrimitiveProps) => React.ReactNode;
15
-
16
14
  styles?: InputStyles;
17
15
  }
18
16
 
@@ -31,16 +29,15 @@ export const InputPrimitive = forwardRef<TextInputRef, InputPrimitiveProps>((pro
31
29
  const state = calculateState(props, isFocused);
32
30
  const field = useFieldOptional();
33
31
 
34
- const composedStyles = [props.styles?.default?.style, props.styles?.[state]?.style, props.style];
32
+ const composedStyles = StyleSheet.flatten([props.styles?.default?.style, props.styles?.[state]?.style, props.style]);
35
33
  const composedProps = {
36
34
  ...props.styles?.default,
37
35
  ...props.styles?.[state],
38
36
  ...props,
39
37
  };
40
- const Component = props.render ?? TextInput;
41
38
 
42
39
  return (
43
- <Component
40
+ <TextInput
44
41
  {...composedProps}
45
42
  ref={ref}
46
43
  id={field?.id}
@@ -1,4 +1,5 @@
1
1
  import type { ViewRef } from "@/types/element.types";
2
+ import { measureLayoutPosition } from "@/utils/normalize-layout";
2
3
  import React, { forwardRef, useImperativeHandle, useRef } from "react";
3
4
  import { type PressableProps } from "react-native";
4
5
  import { usePopover } from "./context";
@@ -18,13 +19,8 @@ export const PopoverTrigger = forwardRef<PopoverTriggerRef, PopoverTriggerProps>
18
19
 
19
20
  const onTriggerPress = async () => {
20
21
  if (!popover.isOpen) {
21
- triggerRef.current?.measureInWindow((pageX, pageY, width, height) => {
22
- popover.setTriggerPosition({
23
- height,
24
- width,
25
- pageX,
26
- pageY,
27
- });
22
+ measureLayoutPosition(triggerRef.current, (layout) => {
23
+ popover.setTriggerPosition(layout);
28
24
  popover.setIsOpen(true);
29
25
  });
30
26
  } else {
@@ -3,8 +3,8 @@ export const DEFAULT_PORTAL_HOST = "__KOR_PORTAL_HOST__";
3
3
  export interface PortalHostProps {
4
4
  name?: string;
5
5
  container?: {
6
- ios?: React.ComponentType<React.PropsWithChildren>;
7
- android?: React.ComponentType<React.PropsWithChildren>;
6
+ ios?: React.ComponentType<{ children: React.ReactNode }>;
7
+ android?: React.ComponentType<{ children: React.ReactNode }>;
8
8
  };
9
9
  }
10
10
 
@@ -1,4 +1,4 @@
1
- import { useEffect, useState, useSyncExternalStore } from "react";
1
+ import { useEffect, useSyncExternalStore } from "react";
2
2
  import { Platform, View } from "react-native";
3
3
  import { DEFAULT_PORTAL_HOST, type PortalHostProps, type PortalProps } from "./portal.constants";
4
4
 
@@ -48,7 +48,7 @@ function removePortal(hostName: string, name: string) {
48
48
  emit();
49
49
  }
50
50
 
51
- function NativePortalHost({ name = DEFAULT_PORTAL_HOST, container }: PortalHostProps) {
51
+ export function PortalHost({ name = DEFAULT_PORTAL_HOST, container }: PortalHostProps) {
52
52
  const map = useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
53
53
  const portalMap = map.get(name) ?? new Map<string, React.ReactNode>();
54
54
  if (portalMap.size === 0) return null;
@@ -69,23 +69,13 @@ function NativePortalHost({ name = DEFAULT_PORTAL_HOST, container }: PortalHostP
69
69
  }}
70
70
  />
71
71
  ),
72
- ios: container?.ios,
73
- android: container?.android,
72
+ ...container,
74
73
  });
75
74
 
76
75
  return <Container>{Array.from(portalMap.values())}</Container>;
77
76
  }
78
77
 
79
- function WebPortalHost() {
80
- return <></>;
81
- }
82
-
83
- export const PortalHost = Platform.select({
84
- default: NativePortalHost,
85
- web: WebPortalHost,
86
- });
87
-
88
- function NativePortal({ name, hostName = DEFAULT_PORTAL_HOST, children }: PortalProps) {
78
+ export function Portal({ name, hostName = DEFAULT_PORTAL_HOST, children }: PortalProps) {
89
79
  useEffect(() => {
90
80
  updatePortal(hostName, name, children);
91
81
  }, [hostName, name, children]);
@@ -98,25 +88,3 @@ function NativePortal({ name, hostName = DEFAULT_PORTAL_HOST, children }: Portal
98
88
 
99
89
  return <></>;
100
90
  }
101
-
102
- function WebPortal({ name, hostName = DEFAULT_PORTAL_HOST, children }: PortalProps) {
103
- const [] = useState(() => {
104
- let container = document.getElementById(hostName);
105
-
106
- if (!container) {
107
- container = document.createElement("div");
108
- container.id = hostName;
109
- document.body.appendChild(container);
110
- }
111
- return container;
112
- });
113
-
114
- const createPortal = require("react-dom").createPortal as typeof import("react-dom").createPortal;
115
-
116
- return <>{createPortal(children, document.body, name)}</>;
117
- }
118
-
119
- export const Portal = Platform.select({
120
- default: NativePortal,
121
- web: WebPortal,
122
- });
@@ -1,3 +1,4 @@
1
+ import type { LayoutPosition } from "@/hooks";
1
2
  import { createContext, type Dispatch, useContext } from "react";
2
3
  import type { LayoutRectangle } from "react-native";
3
4
  import type { SelectOption, SelectState, SelectStyles } from "./types";
@@ -9,8 +10,10 @@ export interface SelectContext {
9
10
 
10
11
  isOpen: boolean;
11
12
  setIsOpen: Dispatch<React.SetStateAction<boolean>>;
12
- triggerLayout: LayoutRectangle | null;
13
- setTriggerLayout: Dispatch<React.SetStateAction<LayoutRectangle | null>>;
13
+ triggerPosition: LayoutPosition;
14
+ setTriggerPosition: Dispatch<React.SetStateAction<LayoutPosition>>;
15
+ contentLayout: LayoutRectangle;
16
+ setContentLayout: Dispatch<React.SetStateAction<LayoutRectangle>>;
14
17
  options: Array<SelectOption>;
15
18
  setOptions: Dispatch<React.SetStateAction<Array<SelectOption>>>;
16
19
 
@@ -1,3 +1,4 @@
1
+ import { useRelativePosition } from "@/hooks/use-relative-position";
1
2
  import { calculateComposedStyles } from "@/utils/calculate-styles";
2
3
  import React from "react";
3
4
  import { type StyleProp, View, type ViewStyle } from "react-native";
@@ -15,18 +16,24 @@ export function SelectContent(props: SelectContentProps) {
15
16
  const select = useSelect();
16
17
  const composedStyles = calculateComposedStyles(select.styles, select.state, "content", props.style);
17
18
 
19
+ const positionStyle = useRelativePosition({
20
+ align: "start",
21
+ avoidCollisions: true,
22
+ triggerPosition: select.triggerPosition,
23
+ contentLayout: select.contentLayout,
24
+ alignOffset: 0,
25
+ side: "bottom",
26
+ sideOffset: 0,
27
+ });
28
+
18
29
  const Component = props.render ?? View;
19
30
  return (
20
31
  <Component
21
- style={[
22
- composedStyles,
23
- {
24
- position: "absolute",
25
- top: select.triggerLayout?.y! + select.triggerLayout?.height!,
26
- left: select.triggerLayout?.x!,
27
- width: select.triggerLayout?.width!,
28
- },
29
- ]}
32
+ style={[positionStyle, composedStyles, { width: select.triggerPosition.width }]}
33
+ onLayout={(e) => {
34
+ select.setContentLayout(e.nativeEvent.layout);
35
+ }}
36
+ pointerEvents="box-none"
30
37
  >
31
38
  {props.children}
32
39
  </Component>
@@ -1,3 +1,4 @@
1
+ import { DEFAULT_LAYOUT, DEFAULT_POSITION, type LayoutPosition } from "@/hooks";
1
2
  import { calculateComposedStyles } from "@/utils/calculate-styles";
2
3
  import React, { useState } from "react";
3
4
  import { type LayoutRectangle, type StyleProp, View, type ViewStyle } from "react-native";
@@ -34,7 +35,8 @@ const calculateState = (props: SelectRootProps): SelectState => {
34
35
 
35
36
  export function SelectRoot(props: SelectRootProps) {
36
37
  const [isOpen, setIsOpen] = useState(false);
37
- const [triggerLayout, setTriggerLayout] = useState<LayoutRectangle | null>(null);
38
+ const [contentLayout, setContentLayout] = useState<LayoutRectangle>(DEFAULT_LAYOUT);
39
+ const [triggerPosition, setTriggerPosition] = useState<LayoutPosition>(DEFAULT_POSITION);
38
40
  const [options, setOptions] = useState<Array<SelectOption>>([]);
39
41
 
40
42
  const state = calculateState(props);
@@ -49,8 +51,10 @@ export function SelectRoot(props: SelectRootProps) {
49
51
  placeholder: props.placeholder,
50
52
  isOpen,
51
53
  setIsOpen,
52
- triggerLayout,
53
- setTriggerLayout,
54
+ triggerPosition,
55
+ setTriggerPosition,
56
+ contentLayout,
57
+ setContentLayout,
54
58
  options,
55
59
  setOptions,
56
60
  state,
@@ -1,38 +1,36 @@
1
+ import type { ViewRef } from "@/types/element.types";
1
2
  import { calculateComposedStyles } from "@/utils/calculate-styles";
2
- import { normalizeLayout } from "@/utils/normalize-layout";
3
- import React from "react";
3
+ import { measureLayoutPosition } from "@/utils/normalize-layout";
4
+ import React, { useRef } from "react";
4
5
  import { Pressable, type StyleProp, type ViewStyle } from "react-native";
5
6
  import { useSelect } from "./context";
6
7
 
7
- interface SelectTriggerInjectionProps {
8
- onPress?: () => void;
9
- }
10
-
11
8
  export interface SelectTriggerProps {
12
9
  children?: React.ReactNode;
13
10
 
14
11
  style?: StyleProp<ViewStyle>;
15
-
16
- render?: (props: SelectTriggerInjectionProps) => React.ReactElement;
17
12
  }
18
13
 
19
14
  export function SelectTrigger(props: SelectTriggerProps) {
20
15
  const select = useSelect();
16
+ const triggerRef = useRef<ViewRef>(null);
17
+
21
18
  const composedStyles = calculateComposedStyles(select.styles, select.state, "trigger", props.style);
22
- const Component = props.render ?? Pressable;
19
+
20
+ const onTriggerPress = () => {
21
+ if (!select.isOpen) {
22
+ measureLayoutPosition(triggerRef.current, (layout) => {
23
+ select.setTriggerPosition(layout);
24
+ select.setIsOpen(true);
25
+ });
26
+ } else {
27
+ select.setIsOpen(false);
28
+ }
29
+ };
30
+
23
31
  return (
24
- <Component
25
- onPress={() => {
26
- select.setIsOpen((prev) => !prev);
27
- }}
28
- onLayout={(e) => {
29
- const layout = normalizeLayout(e.nativeEvent.layout);
30
- select.setTriggerLayout(layout);
31
- }}
32
- disabled={select.isDisabled}
33
- style={composedStyles}
34
- >
32
+ <Pressable ref={triggerRef} onPress={onTriggerPress} disabled={select.isDisabled} style={composedStyles}>
35
33
  {props.children}
36
- </Component>
34
+ </Pressable>
37
35
  );
38
36
  }
@@ -1,4 +1,11 @@
1
- import type { TextInput, View } from "react-native";
1
+ import type { HostInstance } from "react-native";
2
2
 
3
- export type ViewRef = React.ComponentRef<View>;
4
- export type TextInputRef = React.ComponentRef<TextInput>;
3
+ export type ViewRef = HostInstance;
4
+ export type TextInputRef = HostInstance & {
5
+ focus: () => void;
6
+ blur: () => void;
7
+ clear: () => void;
8
+ isFocused: () => boolean;
9
+ getNativeRef: () => HostInstance;
10
+ setSelection: (start: number, end?: number) => void;
11
+ };
@@ -0,0 +1,10 @@
1
+ import type { TextInputRef } from "@/types/element.types";
2
+ import { Platform } from "react-native";
3
+
4
+ export function setInnerInputValue(element: HTMLInputElement | TextInputRef, value: string) {
5
+ if (Platform.OS === "web") {
6
+ (element as HTMLInputElement).value = value;
7
+ } else {
8
+ (element as TextInputRef)?.setNativeProps?.({ text: value });
9
+ }
10
+ }
@@ -1,4 +1,6 @@
1
- import type { LayoutRectangle } from "react-native";
1
+ import type { LayoutPosition } from "@/hooks";
2
+ import type { HostInstance, LayoutRectangle } from "react-native";
3
+
2
4
  export const normalizeLayout = (layout: LayoutRectangle) => {
3
5
  const _layout = { ...layout };
4
6
  // Web layout doesn't provide x/y, but left/top
@@ -10,3 +12,22 @@ export const normalizeLayout = (layout: LayoutRectangle) => {
10
12
  }
11
13
  return _layout;
12
14
  };
15
+
16
+ const isValidNumber = (value: unknown): value is number => {
17
+ const isValid = typeof value === "number" && !isNaN(value) && isFinite(value);
18
+ if (!isValid) {
19
+ console.warn(`Expected a valid number but received: ${value}`);
20
+ }
21
+ return isValid;
22
+ };
23
+
24
+ export const measureLayoutPosition = (ref: HostInstance | null, callback: (layout: LayoutPosition) => void) => {
25
+ ref?.measureInWindow((pageX, pageY, width, height) => {
26
+ callback({
27
+ height: isValidNumber(height) ? height : 0,
28
+ width: isValidNumber(width) ? width : 0,
29
+ pageX: isValidNumber(pageX) ? pageX : 0,
30
+ pageY: isValidNumber(pageY) ? pageY : 0,
31
+ });
32
+ });
33
+ };