@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
@@ -1 +1 @@
1
- {"version":3,"file":"element.types.d.ts","sourceRoot":"","sources":["../../../../src/types/element.types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,IAAI,EAAE,MAAM,cAAc,CAAC;AAEpD,MAAM,MAAM,OAAO,GAAG,KAAK,CAAC,YAAY,CAAC,IAAI,CAAC,CAAC;AAC/C,MAAM,MAAM,YAAY,GAAG,KAAK,CAAC,YAAY,CAAC,SAAS,CAAC,CAAC"}
1
+ {"version":3,"file":"element.types.d.ts","sourceRoot":"","sources":["../../../../src/types/element.types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAEjD,MAAM,MAAM,OAAO,GAAG,YAAY,CAAC;AACnC,MAAM,MAAM,YAAY,GAAG,YAAY,GAAG;IACxC,KAAK,EAAE,MAAM,IAAI,CAAC;IAClB,IAAI,EAAE,MAAM,IAAI,CAAC;IACjB,KAAK,EAAE,MAAM,IAAI,CAAC;IAClB,SAAS,EAAE,MAAM,OAAO,CAAC;IACzB,YAAY,EAAE,MAAM,YAAY,CAAC;IACjC,YAAY,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,CAAC,EAAE,MAAM,KAAK,IAAI,CAAC;CACrD,CAAC"}
@@ -0,0 +1,3 @@
1
+ import type { TextInputRef } from "../types/element.types";
2
+ export declare function setInnerInputValue(element: HTMLInputElement | TextInputRef, value: string): void;
3
+ //# sourceMappingURL=input-utils.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"input-utils.d.ts","sourceRoot":"","sources":["../../../../src/utils/input-utils.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,uBAAuB,CAAC;AAG1D,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,gBAAgB,GAAG,YAAY,EAAE,KAAK,EAAE,MAAM,QAMzF"}
@@ -1,8 +1,10 @@
1
- import type { LayoutRectangle } from "react-native";
1
+ import type { LayoutPosition } from "../hooks";
2
+ import type { HostInstance, LayoutRectangle } from "react-native";
2
3
  export declare const normalizeLayout: (layout: LayoutRectangle) => {
3
4
  x: number;
4
5
  y: number;
5
6
  width: number;
6
7
  height: number;
7
8
  };
9
+ export declare const measureLayoutPosition: (ref: HostInstance | null, callback: (layout: LayoutPosition) => void) => void;
8
10
  //# sourceMappingURL=normalize-layout.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"normalize-layout.d.ts","sourceRoot":"","sources":["../../../../src/utils/normalize-layout.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC;AACpD,eAAO,MAAM,eAAe,GAAI,QAAQ,eAAe;;;;;CAUtD,CAAC"}
1
+ {"version":3,"file":"normalize-layout.d.ts","sourceRoot":"","sources":["../../../../src/utils/normalize-layout.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,SAAS,CAAC;AAC9C,OAAO,KAAK,EAAE,YAAY,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC;AAElE,eAAO,MAAM,eAAe,GAAI,QAAQ,eAAe;;;;;CAUtD,CAAC;AAUF,eAAO,MAAM,qBAAqB,GAAI,KAAK,YAAY,GAAG,IAAI,EAAE,UAAU,CAAC,MAAM,EAAE,cAAc,KAAK,IAAI,SASzG,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@korsolutions/ui",
3
- "version": "0.0.38",
3
+ "version": "0.0.40",
4
4
  "license": "MIT",
5
5
  "repository": {
6
6
  "type": "git",
@@ -35,20 +35,23 @@
35
35
  "types": "./dist/typescript/src/primitives/index.d.ts"
36
36
  }
37
37
  },
38
+ "scripts": {
39
+ "prepare": "./scripts/build.sh",
40
+ "ts-check": "tsc --noEmit"
41
+ },
38
42
  "devDependencies": {
43
+ "@react-navigation/native": "^7.1.28",
39
44
  "@types/react": "^19.2.3",
40
45
  "@types/react-dom": "^19.2.3",
41
46
  "babel-plugin-module-resolver": "^5.0.2",
42
47
  "react-native-builder-bob": "^0.40.17",
43
- "tsc-alias": "^1.8.16"
48
+ "tsc-alias": "^1.8.16",
49
+ "typescript": "^5.9.3"
44
50
  },
45
51
  "peerDependencies": {
46
52
  "react": "*",
47
53
  "react-dom": "*",
48
54
  "react-native": "*",
49
55
  "react-native-web": "*"
50
- },
51
- "scripts": {
52
- "ts-check": "tsc --noEmit"
53
56
  }
54
57
  }
package/scripts/build.sh CHANGED
File without changes
@@ -0,0 +1,34 @@
1
+ import { AutocompletePrimitive, type AutocompleteOption, type AutocompleteRootBaseProps } from "@/primitives";
2
+ import React from "react";
3
+ import { AutocompleteVariants } from "./variants";
4
+
5
+ export type { AutocompleteOption };
6
+
7
+ export interface AutocompleteProps extends AutocompleteRootBaseProps {
8
+ options: AutocompleteOption[];
9
+ variant?: keyof typeof AutocompleteVariants;
10
+ emptyMessage?: string;
11
+ }
12
+
13
+ export function Autocomplete(props: AutocompleteProps) {
14
+ const { options, variant = "default", emptyMessage = "No results found", ...rootProps } = props;
15
+ const useVariantStyles = AutocompleteVariants[variant];
16
+ const variantStyles = useVariantStyles();
17
+
18
+ return (
19
+ <AutocompletePrimitive.Root {...rootProps} styles={variantStyles}>
20
+ <AutocompletePrimitive.Input />
21
+ <AutocompletePrimitive.Portal>
22
+ <AutocompletePrimitive.Overlay />
23
+ <AutocompletePrimitive.Content>
24
+ {options.map((option) => (
25
+ <AutocompletePrimitive.Option key={option.value} value={option.value}>
26
+ {option.label}
27
+ </AutocompletePrimitive.Option>
28
+ ))}
29
+ {!options.length && <AutocompletePrimitive.Empty>{emptyMessage}</AutocompletePrimitive.Empty>}
30
+ </AutocompletePrimitive.Content>
31
+ </AutocompletePrimitive.Portal>
32
+ </AutocompletePrimitive.Root>
33
+ );
34
+ }
@@ -0,0 +1,84 @@
1
+ import { type AutocompleteStyles } from "@/primitives";
2
+ import { useThemedStyles } from "@/utils/use-themed-styles";
3
+
4
+ export function useAutocompleteVariantDefault(): AutocompleteStyles {
5
+ return useThemedStyles(
6
+ ({ colors, radius, fontFamily, fontSize }): AutocompleteStyles => ({
7
+ root: {
8
+ default: {},
9
+ focused: {},
10
+ disabled: {},
11
+ },
12
+ input: {
13
+ default: {
14
+ borderWidth: 1,
15
+ borderColor: colors.border,
16
+ borderRadius: radius,
17
+ paddingVertical: 12,
18
+ paddingHorizontal: 16,
19
+ height: 48,
20
+ fontFamily,
21
+ fontSize,
22
+ color: colors.foreground,
23
+ backgroundColor: colors.background,
24
+ },
25
+ focused: {
26
+ borderColor: colors.primary,
27
+ },
28
+ disabled: {
29
+ backgroundColor: colors.muted,
30
+ color: colors.mutedForeground,
31
+ },
32
+ },
33
+ overlay: {
34
+ default: {},
35
+ focused: {},
36
+ disabled: {},
37
+ },
38
+ content: {
39
+ default: {
40
+ backgroundColor: colors.surface,
41
+ borderRadius: radius,
42
+ borderWidth: 1,
43
+ borderColor: colors.border,
44
+ padding: 4,
45
+ gap: 4,
46
+ maxHeight: 300,
47
+ },
48
+ focused: {},
49
+ disabled: {},
50
+ },
51
+ option: {
52
+ default: {
53
+ paddingVertical: 12,
54
+ paddingHorizontal: 16,
55
+ fontFamily,
56
+ fontSize,
57
+ color: colors.foreground,
58
+ borderRadius: radius / 2,
59
+ },
60
+ disabled: {
61
+ color: colors.mutedForeground,
62
+ },
63
+ selected: {
64
+ backgroundColor: colors.muted,
65
+ },
66
+ hovered: {
67
+ backgroundColor: colors.muted,
68
+ },
69
+ },
70
+ empty: {
71
+ default: {
72
+ paddingVertical: 12,
73
+ paddingHorizontal: 16,
74
+ fontFamily,
75
+ fontSize,
76
+ color: colors.mutedForeground,
77
+ textAlign: "center",
78
+ },
79
+ focused: {},
80
+ disabled: {},
81
+ },
82
+ })
83
+ );
84
+ }
@@ -0,0 +1,5 @@
1
+ import { useAutocompleteVariantDefault } from "./default";
2
+
3
+ export const AutocompleteVariants = {
4
+ default: useAutocompleteVariantDefault,
5
+ };
@@ -3,6 +3,7 @@ export * from "./card/card";
3
3
  export * from "./input";
4
4
  export * from "./field/field";
5
5
  export * from "./select/select";
6
+ export * from "./autocomplete/autocomplete";
6
7
  export * from "./typography/typography";
7
8
  export * from "./link/link";
8
9
  export * from "./empty/empty";
@@ -1,10 +1,10 @@
1
1
  import { useNumericMask, type NumericMaskFormat } from "@/hooks/use-numeric-mask";
2
- import { type InputPrimitiveBaseProps } from "@/primitives";
2
+ import { InputPrimitive, type InputPrimitiveBaseProps } from "@/primitives";
3
3
  import React, { useEffect } from "react";
4
- import { Input } from "./input";
4
+ import { InputVariants } from "./variants";
5
5
 
6
6
  export interface NumericInputProps extends Omit<InputPrimitiveBaseProps, "value" | "onChange" | "keyboardType"> {
7
- variant?: "default";
7
+ variant?: keyof typeof InputVariants;
8
8
  value?: number | null;
9
9
  onChange?: (value: number | null) => void;
10
10
  format?: NumericMaskFormat;
@@ -42,7 +42,6 @@ export function NumericInput({
42
42
  onChange,
43
43
  });
44
44
 
45
- // Sync external value changes with internal state
46
45
  useEffect(() => {
47
46
  if (value !== numericMask.numericValue) {
48
47
  numericMask.setValue(value ?? null);
@@ -59,15 +58,18 @@ export function NumericInput({
59
58
  onFocus?.(e);
60
59
  };
61
60
 
61
+ const useVariantStyles = InputVariants[variant];
62
+ const variantStyles = useVariantStyles();
63
+
62
64
  return (
63
- <Input
65
+ <InputPrimitive
64
66
  {...props}
65
- variant={variant}
66
67
  value={numericMask.value}
67
68
  onChange={numericMask.onChangeText}
68
69
  onBlur={handleBlur}
69
70
  onFocus={handleFocus}
70
71
  keyboardType={numericMask.keyboardType}
72
+ styles={variantStyles}
71
73
  />
72
74
  );
73
75
  }
@@ -7,7 +7,7 @@ export interface AlertDialogPrimitiveTriggerProps extends PressableProps {
7
7
  children: React.ReactElement<React.RefAttributes<ViewRef> & PressableProps>;
8
8
  }
9
9
 
10
- export function AlertDialogTrigger(props: AlertDialogPrimitiveTriggerProps) {
10
+ export function AlertDialogTrigger(props: AlertDialogPrimitiveTriggerProps): React.ReactElement {
11
11
  const { onPress } = props;
12
12
  const { isOpen, setIsOpen } = useAlertDialog();
13
13
 
@@ -0,0 +1,38 @@
1
+ import { useRelativePosition } from "@/hooks/use-relative-position";
2
+ import { calculateComposedStyles } from "@/utils/calculate-styles";
3
+ import React from "react";
4
+ import { ScrollView, type StyleProp, type ViewStyle } from "react-native";
5
+ import { useAutocomplete } from "./context";
6
+
7
+ export interface AutocompleteContentProps {
8
+ children?: React.ReactNode;
9
+ style?: StyleProp<ViewStyle>;
10
+ }
11
+
12
+ export function AutocompleteContent(props: AutocompleteContentProps) {
13
+ const autocomplete = useAutocomplete();
14
+ const composedStyles = calculateComposedStyles(autocomplete.styles, autocomplete.state, "content", props.style);
15
+
16
+ const positionStyle = useRelativePosition({
17
+ align: "start",
18
+ avoidCollisions: true,
19
+ triggerPosition: autocomplete.inputPosition,
20
+ contentLayout: autocomplete.contentLayout,
21
+ alignOffset: 0,
22
+ side: "bottom",
23
+ sideOffset: 0,
24
+ });
25
+
26
+ return (
27
+ <ScrollView
28
+ style={[positionStyle, composedStyles, { width: autocomplete.inputPosition.width }]}
29
+ onLayout={(e) => {
30
+ autocomplete.setContentLayout(e.nativeEvent.layout);
31
+ }}
32
+ pointerEvents="box-none"
33
+ keyboardShouldPersistTaps="handled"
34
+ >
35
+ {props.children}
36
+ </ScrollView>
37
+ );
38
+ }
@@ -0,0 +1,19 @@
1
+ import { calculateComposedStyles } from "@/utils/calculate-styles";
2
+ import React from "react";
3
+ import { type StyleProp, Text, type TextStyle } from "react-native";
4
+ import { useAutocomplete } from "./context";
5
+
6
+ export interface AutocompleteEmptyProps {
7
+ children?: React.ReactNode;
8
+ style?: StyleProp<TextStyle>;
9
+ render?: (props: AutocompleteEmptyProps) => React.ReactElement;
10
+ }
11
+
12
+ export function AutocompleteEmpty(props: AutocompleteEmptyProps) {
13
+ const autocomplete = useAutocomplete();
14
+
15
+ const composedStyles = calculateComposedStyles(autocomplete.styles, autocomplete.state, "empty", props.style);
16
+
17
+ const Component = props.render ?? Text;
18
+ return <Component style={composedStyles}>{props.children}</Component>;
19
+ }
@@ -0,0 +1,86 @@
1
+ import type { TextInputRef } from "@/types/element.types";
2
+ import { setInnerInputValue } from "@/utils/input-utils";
3
+ import { measureLayoutPosition } from "@/utils/normalize-layout";
4
+ import { forwardRef, useEffect, useImperativeHandle, useRef } from "react";
5
+ import { StyleSheet, TextInput, type StyleProp, type TextInputProps, type TextStyle } from "react-native";
6
+ import { useFieldOptional } from "../field/context";
7
+ import { useAutocomplete } from "./context";
8
+
9
+ export interface AutocompleteInputProps extends Omit<TextInputProps, "onChange" | "value"> {
10
+ style?: StyleProp<TextStyle>;
11
+ }
12
+
13
+ export const AutocompleteInput = forwardRef<TextInputRef, AutocompleteInputProps>((props, ref) => {
14
+ const autocomplete = useAutocomplete();
15
+ const field = useFieldOptional();
16
+ const inputRef = useRef<TextInputRef>(null);
17
+ const blurTimeoutRef = useRef<NodeJS.Timeout | undefined>(undefined);
18
+
19
+ const composedStyles = [autocomplete.styles?.input?.default, autocomplete.styles?.input?.[autocomplete.state], props.style];
20
+
21
+ const handleFocus: TextInputProps["onFocus"] = (e) => {
22
+ // Clear any pending blur timeout
23
+ if (blurTimeoutRef.current) {
24
+ clearTimeout(blurTimeoutRef.current);
25
+ }
26
+
27
+ // Measure layout position and open dropdown
28
+ if (autocomplete.openOnFocus && !autocomplete.isDisabled) {
29
+ measureLayoutPosition(inputRef.current, (layout) => {
30
+ autocomplete.setInputPosition(layout);
31
+ autocomplete.setIsOpen(true);
32
+ });
33
+ }
34
+
35
+ props.onFocus?.(e);
36
+ };
37
+
38
+ const handleBlur: TextInputProps["onBlur"] = (e) => {
39
+ blurTimeoutRef.current = setTimeout(() => {
40
+ autocomplete.setIsOpen(false);
41
+ }, 150);
42
+
43
+ props.onBlur?.(e);
44
+ };
45
+
46
+ const handleChangeText = (text: string) => {
47
+ autocomplete.setInputValue?.(text);
48
+
49
+ // Open dropdown when typing
50
+ if (!autocomplete.isOpen && text.trim() && autocomplete.openOnFocus) {
51
+ measureLayoutPosition(inputRef.current, (layout) => {
52
+ autocomplete.setInputPosition(layout);
53
+ autocomplete.setIsOpen(true);
54
+ });
55
+ }
56
+
57
+ props.onChangeText?.(text);
58
+ };
59
+
60
+ useImperativeHandle(ref, () => inputRef.current!);
61
+ useEffect(() => {
62
+ autocomplete.setBlurInput(() => () => {
63
+ inputRef.current?.blur();
64
+ });
65
+ autocomplete.setInputDisplayValueSetter(() => (v: string) => {
66
+ setInnerInputValue(inputRef.current!, v);
67
+ });
68
+ }, []);
69
+
70
+ return (
71
+ <TextInput
72
+ {...props}
73
+ ref={inputRef}
74
+ id={field?.id}
75
+ value={autocomplete.inputValue}
76
+ placeholder={autocomplete.placeholder}
77
+ onChangeText={handleChangeText}
78
+ onFocus={handleFocus}
79
+ onBlur={handleBlur}
80
+ editable={!autocomplete.isDisabled}
81
+ style={StyleSheet.flatten(composedStyles)}
82
+ />
83
+ );
84
+ });
85
+
86
+ AutocompleteInput.displayName = "AutocompleteInput";
@@ -0,0 +1,66 @@
1
+ import { calculateComposedStyles } from "@/utils/calculate-styles";
2
+ import { useEffect, useState } from "react";
3
+ import { type StyleProp, Text, type TextStyle } from "react-native";
4
+ import { useAutocomplete } from "./context";
5
+ import type { AutocompleteOptionState, AutocompleteState } from "./types";
6
+
7
+ export interface AutocompleteOptionProps {
8
+ children: string;
9
+ value: string;
10
+ onMouseEnter?: () => void;
11
+ onMouseLeave?: () => void;
12
+ render?: (props: AutocompleteOptionProps) => React.ReactElement;
13
+ style?: StyleProp<TextStyle>;
14
+ }
15
+
16
+ const calculateState = (autocompleteState: AutocompleteState, hovered: boolean, selected: boolean): AutocompleteOptionState => {
17
+ if (autocompleteState === "disabled") {
18
+ return "disabled";
19
+ }
20
+ if (selected) {
21
+ return "selected";
22
+ }
23
+ if (hovered) {
24
+ return "hovered";
25
+ }
26
+ return "default";
27
+ };
28
+
29
+ export function AutocompleteOption(props: AutocompleteOptionProps) {
30
+ const [isHovered, setIsHovered] = useState(false);
31
+ const autocomplete = useAutocomplete();
32
+ const isSelected = autocomplete.value === props.value;
33
+
34
+ const optionState = calculateState(autocomplete.state, isHovered, isSelected);
35
+ const composedStyles = calculateComposedStyles(autocomplete.styles, optionState, "option", props.style);
36
+
37
+ useEffect(() => {
38
+ autocomplete.setOptions((prev) => {
39
+ if (prev.find((option) => option.value === props.value)) {
40
+ return prev;
41
+ }
42
+ return [...prev, { value: props.value, label: props.children }];
43
+ });
44
+ }, [props.value, props.children]);
45
+
46
+ const handlePress = () => {
47
+ autocomplete.onChange?.(props.value);
48
+ autocomplete.setInputValue?.("");
49
+ autocomplete.setInputDisplayValue(props.children);
50
+ autocomplete.setIsOpen(false);
51
+ autocomplete.blurInput();
52
+ };
53
+
54
+ const Component = props.render ?? Text;
55
+ return (
56
+ <Component
57
+ value={props.value}
58
+ onPress={handlePress}
59
+ onMouseEnter={() => setIsHovered(true)}
60
+ onMouseLeave={() => setIsHovered(false)}
61
+ style={composedStyles}
62
+ >
63
+ {props.children}
64
+ </Component>
65
+ );
66
+ }
@@ -0,0 +1,29 @@
1
+ import { calculateComposedStyles } from "@/utils/calculate-styles";
2
+ import React from "react";
3
+ import { Pressable, type StyleProp, StyleSheet, type ViewStyle } from "react-native";
4
+ import { useAutocomplete } from "./context";
5
+
6
+ export interface AutocompleteOverlayProps {
7
+ children?: React.ReactNode;
8
+ onPress?: () => void;
9
+ style?: StyleProp<ViewStyle>;
10
+ render?: (props: AutocompleteOverlayProps) => React.ReactElement;
11
+ }
12
+
13
+ export function AutocompleteOverlay(props: AutocompleteOverlayProps) {
14
+ const autocomplete = useAutocomplete();
15
+
16
+ const composedStyles = calculateComposedStyles(autocomplete.styles, autocomplete.state, "overlay", props.style);
17
+
18
+ const Component = props.render ?? Pressable;
19
+ return (
20
+ <Component
21
+ onPress={() => {
22
+ autocomplete.setIsOpen(false);
23
+ }}
24
+ style={[StyleSheet.absoluteFill, composedStyles]}
25
+ >
26
+ {props.children}
27
+ </Component>
28
+ );
29
+ }
@@ -0,0 +1,27 @@
1
+ import React, { useEffect } from "react";
2
+ import { Portal } from "../portal";
3
+ import { useAutocomplete, AutocompleteContext } from "./context";
4
+
5
+ export interface AutocompletePortalProps {
6
+ children?: React.ReactNode;
7
+ }
8
+
9
+ export function AutocompletePortal(props: AutocompletePortalProps) {
10
+ const autocomplete = useAutocomplete();
11
+
12
+ useEffect(() => {
13
+ return () => {
14
+ autocomplete.setOptions([]);
15
+ };
16
+ }, []);
17
+
18
+ if (!autocomplete.isOpen) {
19
+ return null;
20
+ }
21
+
22
+ return (
23
+ <Portal name="autocomplete-portal">
24
+ <AutocompleteContext.Provider value={autocomplete}>{props.children}</AutocompleteContext.Provider>
25
+ </Portal>
26
+ );
27
+ }
@@ -0,0 +1,111 @@
1
+ import { DEFAULT_LAYOUT, DEFAULT_POSITION, type LayoutPosition } from "@/hooks";
2
+ import { calculateComposedStyles } from "@/utils/calculate-styles";
3
+ import React, { useEffect, useMemo, useState } from "react";
4
+ import { type LayoutRectangle, type StyleProp, View, type ViewStyle } from "react-native";
5
+ import { AutocompleteContext } from "./context";
6
+ import type { AutocompleteOption, AutocompleteState, AutocompleteStyles } from "./types";
7
+
8
+ interface AutocompleteRootInjectedProps {
9
+ style?: StyleProp<ViewStyle>;
10
+ }
11
+
12
+ export interface AutocompleteRootBaseProps {
13
+ value?: string;
14
+ onChange?: (value: string) => void;
15
+ inputValue?: string;
16
+ setInputValue?: (value: string) => void;
17
+ placeholder?: string;
18
+ isDisabled?: boolean;
19
+ openOnFocus?: boolean;
20
+ }
21
+
22
+ export interface AutocompleteRootProps extends AutocompleteRootBaseProps {
23
+ children?: React.ReactNode;
24
+ render?: (props: AutocompleteRootInjectedProps) => React.ReactElement;
25
+ styles?: AutocompleteStyles;
26
+ style?: StyleProp<ViewStyle>;
27
+ }
28
+
29
+ const calculateState = (props: AutocompleteRootProps, isFocused: boolean): AutocompleteState => {
30
+ if (props.isDisabled) {
31
+ return "disabled";
32
+ }
33
+ if (isFocused) {
34
+ return "focused";
35
+ }
36
+ return "default";
37
+ };
38
+
39
+ export function AutocompleteRoot(props: AutocompleteRootProps) {
40
+ const [isOpen, setIsOpen] = useState(false);
41
+ const [contentLayout, setContentLayout] = useState<LayoutRectangle>(DEFAULT_LAYOUT);
42
+ const [inputPosition, setInputPosition] = useState<LayoutPosition>(DEFAULT_POSITION);
43
+ const [options, setOptions] = useState<Array<AutocompleteOption>>([]);
44
+ const [blurInput, setBlurInput] = useState<() => void>(() => () => {});
45
+ const [setInputDisplayValue, setInputDisplayValueSetter] = useState<(value: string) => void>(() => () => {});
46
+
47
+ useEffect(() => {
48
+ if (props.value) {
49
+ const selectedOption = options.find((opt) => opt.value === props.value);
50
+ if (selectedOption) {
51
+ props.setInputValue?.(selectedOption.label);
52
+ }
53
+ } else {
54
+ props.setInputValue?.("");
55
+ }
56
+ }, [props.value, options]);
57
+
58
+ const state = calculateState(props, isOpen);
59
+ const composedStyles = calculateComposedStyles(props.styles, state, "root", props.style);
60
+
61
+ const contextValue = useMemo(
62
+ () =>
63
+ ({
64
+ value: props.value,
65
+ onChange: props.onChange,
66
+ placeholder: props.placeholder,
67
+ inputValue: props.inputValue,
68
+ setInputValue: props.setInputValue,
69
+ isOpen,
70
+ setIsOpen,
71
+ inputPosition,
72
+ setInputPosition,
73
+ contentLayout,
74
+ setContentLayout,
75
+ options,
76
+ setOptions,
77
+ openOnFocus: props.openOnFocus ?? true,
78
+ blurInput,
79
+ setBlurInput,
80
+ setInputDisplayValue,
81
+ setInputDisplayValueSetter,
82
+ state,
83
+ isDisabled: props.isDisabled ?? false,
84
+ styles: props.styles ?? null,
85
+ }) satisfies AutocompleteContext,
86
+ [
87
+ props.value,
88
+ props.onChange,
89
+ props.placeholder,
90
+ props.openOnFocus,
91
+ props.isDisabled,
92
+ props.styles,
93
+ props.inputValue,
94
+ props.setInputValue,
95
+ isOpen,
96
+ inputPosition,
97
+ contentLayout,
98
+ options,
99
+ blurInput,
100
+ setInputDisplayValue,
101
+ state,
102
+ ],
103
+ );
104
+
105
+ const Component = props.render ?? View;
106
+ return (
107
+ <AutocompleteContext.Provider value={contextValue}>
108
+ <Component style={composedStyles}>{props.children}</Component>
109
+ </AutocompleteContext.Provider>
110
+ );
111
+ }