@korsolutions/ui 0.0.64 → 0.0.66

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 (117) hide show
  1. package/dist/module/components/button/button.js +44 -0
  2. package/dist/module/components/button/button.js.map +1 -0
  3. package/dist/module/components/button/index.js +1 -8
  4. package/dist/module/components/button/index.js.map +1 -1
  5. package/dist/module/components/button/variants/default.js +15 -10
  6. package/dist/module/components/button/variants/default.js.map +1 -1
  7. package/dist/module/components/button/variants/ghost.js +19 -4
  8. package/dist/module/components/button/variants/ghost.js.map +1 -1
  9. package/dist/module/components/button/variants/secondary.js +19 -4
  10. package/dist/module/components/button/variants/secondary.js.map +1 -1
  11. package/dist/module/components/combobox/components/combobox-content.js +32 -0
  12. package/dist/module/components/combobox/components/combobox-content.js.map +1 -0
  13. package/dist/module/components/combobox/components/combobox-empty.js +20 -0
  14. package/dist/module/components/combobox/components/combobox-empty.js.map +1 -0
  15. package/dist/module/components/combobox/components/combobox-option.js +56 -0
  16. package/dist/module/components/combobox/components/combobox-option.js.map +1 -0
  17. package/dist/module/components/combobox/components/combobox-overlay.js +20 -0
  18. package/dist/module/components/combobox/components/combobox-overlay.js.map +1 -0
  19. package/dist/module/components/combobox/components/combobox-portal.js +29 -0
  20. package/dist/module/components/combobox/components/combobox-portal.js.map +1 -0
  21. package/dist/module/components/combobox/components/combobox-root.js +79 -0
  22. package/dist/module/components/combobox/components/combobox-root.js.map +1 -0
  23. package/dist/module/components/combobox/components/combobox-trigger.js +67 -0
  24. package/dist/module/components/combobox/components/combobox-trigger.js.map +1 -0
  25. package/dist/module/components/combobox/context.js +12 -0
  26. package/dist/module/components/combobox/context.js.map +1 -0
  27. package/dist/module/components/combobox/index.js +19 -0
  28. package/dist/module/components/combobox/index.js.map +1 -0
  29. package/dist/module/components/combobox/types.js +4 -0
  30. package/dist/module/components/combobox/types.js.map +1 -0
  31. package/dist/module/components/combobox/variants/default.js +106 -0
  32. package/dist/module/components/combobox/variants/default.js.map +1 -0
  33. package/dist/module/components/combobox/variants/index.js +7 -0
  34. package/dist/module/components/combobox/variants/index.js.map +1 -0
  35. package/dist/module/components/index.js +1 -0
  36. package/dist/module/components/index.js.map +1 -1
  37. package/dist/module/hooks/use-organized-children.js.map +1 -1
  38. package/dist/typescript/src/components/button/{components/button-root.d.ts → button.d.ts} +4 -4
  39. package/dist/typescript/src/components/button/button.d.ts.map +1 -0
  40. package/dist/typescript/src/components/button/index.d.ts +1 -11
  41. package/dist/typescript/src/components/button/index.d.ts.map +1 -1
  42. package/dist/typescript/src/components/button/types.d.ts +6 -6
  43. package/dist/typescript/src/components/button/types.d.ts.map +1 -1
  44. package/dist/typescript/src/components/button/variants/default.d.ts +1 -1
  45. package/dist/typescript/src/components/button/variants/default.d.ts.map +1 -1
  46. package/dist/typescript/src/components/button/variants/ghost.d.ts +1 -1
  47. package/dist/typescript/src/components/button/variants/ghost.d.ts.map +1 -1
  48. package/dist/typescript/src/components/button/variants/secondary.d.ts +1 -1
  49. package/dist/typescript/src/components/button/variants/secondary.d.ts.map +1 -1
  50. package/dist/typescript/src/components/combobox/components/combobox-content.d.ts +8 -0
  51. package/dist/typescript/src/components/combobox/components/combobox-content.d.ts.map +1 -0
  52. package/dist/typescript/src/components/combobox/components/combobox-empty.d.ts +6 -0
  53. package/dist/typescript/src/components/combobox/components/combobox-empty.d.ts.map +1 -0
  54. package/dist/typescript/src/components/combobox/components/combobox-option.d.ts +8 -0
  55. package/dist/typescript/src/components/combobox/components/combobox-option.d.ts.map +1 -0
  56. package/dist/typescript/src/components/combobox/components/combobox-overlay.d.ts +8 -0
  57. package/dist/typescript/src/components/combobox/components/combobox-overlay.d.ts.map +1 -0
  58. package/dist/typescript/src/components/combobox/components/combobox-portal.d.ts +6 -0
  59. package/dist/typescript/src/components/combobox/components/combobox-portal.d.ts.map +1 -0
  60. package/dist/typescript/src/components/combobox/components/combobox-root.d.ts +19 -0
  61. package/dist/typescript/src/components/combobox/components/combobox-root.d.ts.map +1 -0
  62. package/dist/typescript/src/components/combobox/components/combobox-trigger.d.ts +6 -0
  63. package/dist/typescript/src/components/combobox/components/combobox-trigger.d.ts.map +1 -0
  64. package/dist/typescript/src/components/combobox/context.d.ts +25 -0
  65. package/dist/typescript/src/components/combobox/context.d.ts.map +1 -0
  66. package/dist/typescript/src/components/combobox/index.d.ts +25 -0
  67. package/dist/typescript/src/components/combobox/index.d.ts.map +1 -0
  68. package/dist/typescript/src/components/combobox/types.d.ts +21 -0
  69. package/dist/typescript/src/components/combobox/types.d.ts.map +1 -0
  70. package/dist/typescript/src/components/combobox/variants/default.d.ts +3 -0
  71. package/dist/typescript/src/components/combobox/variants/default.d.ts.map +1 -0
  72. package/dist/typescript/src/components/combobox/variants/index.d.ts +5 -0
  73. package/dist/typescript/src/components/combobox/variants/index.d.ts.map +1 -0
  74. package/dist/typescript/src/components/index.d.ts +1 -0
  75. package/dist/typescript/src/components/index.d.ts.map +1 -1
  76. package/dist/typescript/src/hooks/use-organized-children.d.ts +1 -1
  77. package/dist/typescript/src/hooks/use-organized-children.d.ts.map +1 -1
  78. package/package.json +1 -1
  79. package/src/components/button/button.tsx +85 -0
  80. package/src/components/button/index.ts +1 -13
  81. package/src/components/button/types.ts +10 -6
  82. package/src/components/button/variants/default.tsx +12 -6
  83. package/src/components/button/variants/ghost.tsx +18 -2
  84. package/src/components/button/variants/secondary.tsx +18 -2
  85. package/src/components/combobox/components/combobox-content.tsx +51 -0
  86. package/src/components/combobox/components/combobox-empty.tsx +28 -0
  87. package/src/components/combobox/components/combobox-option.tsx +81 -0
  88. package/src/components/combobox/components/combobox-overlay.tsx +36 -0
  89. package/src/components/combobox/components/combobox-portal.tsx +33 -0
  90. package/src/components/combobox/components/combobox-root.tsx +152 -0
  91. package/src/components/combobox/components/combobox-trigger.tsx +96 -0
  92. package/src/components/combobox/context.ts +40 -0
  93. package/src/components/combobox/index.ts +26 -0
  94. package/src/components/combobox/types.ts +23 -0
  95. package/src/components/combobox/variants/default.tsx +102 -0
  96. package/src/components/combobox/variants/index.ts +5 -0
  97. package/src/components/index.ts +1 -0
  98. package/src/hooks/use-organized-children.tsx +1 -1
  99. package/dist/module/components/button/components/button-label.js +0 -18
  100. package/dist/module/components/button/components/button-label.js.map +0 -1
  101. package/dist/module/components/button/components/button-root.js +0 -60
  102. package/dist/module/components/button/components/button-root.js.map +0 -1
  103. package/dist/module/components/button/components/button-spinner.js +0 -15
  104. package/dist/module/components/button/components/button-spinner.js.map +0 -1
  105. package/dist/module/components/button/context.js +0 -12
  106. package/dist/module/components/button/context.js.map +0 -1
  107. package/dist/typescript/src/components/button/components/button-label.d.ts +0 -9
  108. package/dist/typescript/src/components/button/components/button-label.d.ts.map +0 -1
  109. package/dist/typescript/src/components/button/components/button-root.d.ts.map +0 -1
  110. package/dist/typescript/src/components/button/components/button-spinner.d.ts +0 -8
  111. package/dist/typescript/src/components/button/components/button-spinner.d.ts.map +0 -1
  112. package/dist/typescript/src/components/button/context.d.ts +0 -8
  113. package/dist/typescript/src/components/button/context.d.ts.map +0 -1
  114. package/src/components/button/components/button-label.tsx +0 -25
  115. package/src/components/button/components/button-root.tsx +0 -76
  116. package/src/components/button/components/button-spinner.tsx +0 -14
  117. package/src/components/button/context.ts +0 -17
@@ -0,0 +1,85 @@
1
+ import React, { useState } from "react";
2
+ import {
3
+ ActivityIndicator,
4
+ Pressable,
5
+ type PressableProps,
6
+ type StyleProp,
7
+ StyleSheet,
8
+ type ViewStyle,
9
+ } from "react-native";
10
+ import { useOrganizedChildren } from "../../hooks/use-organized-children";
11
+ import type { ButtonState } from "./types";
12
+ import { ButtonVariants } from "./variants";
13
+
14
+ export interface ButtonProps extends PressableProps {
15
+ variant?: keyof typeof ButtonVariants;
16
+ children?: React.ReactNode;
17
+
18
+ isDisabled?: boolean;
19
+ isLoading?: boolean;
20
+
21
+ style?: StyleProp<ViewStyle>;
22
+ }
23
+
24
+ const calculateState = (
25
+ props: ButtonProps,
26
+ isHovered: boolean,
27
+ ): ButtonState => {
28
+ if (props.isDisabled) return "disabled";
29
+ if (props.isLoading) return "loading";
30
+ if (isHovered) return "hovered";
31
+ return "default";
32
+ };
33
+
34
+ export function Button(props: ButtonProps) {
35
+ const variantStyles = ButtonVariants[props.variant ?? "default"]();
36
+ const [isHovered, setIsHovered] = useState(false);
37
+
38
+ const state = calculateState(props, isHovered);
39
+
40
+ const textStyle = StyleSheet.flatten([
41
+ variantStyles.text?.default,
42
+ variantStyles.text?.[state],
43
+ ]);
44
+ const iconProps = StyleSheet.flatten([
45
+ variantStyles.icon?.default,
46
+ variantStyles.icon?.[state],
47
+ ]);
48
+
49
+ const organizedChildren = useOrganizedChildren(
50
+ props.children,
51
+ textStyle,
52
+ iconProps,
53
+ );
54
+
55
+ const handlePress: PressableProps["onPress"] = (event) => {
56
+ if (props.isDisabled || props.isLoading) {
57
+ event.preventDefault();
58
+ return;
59
+ }
60
+ props.onPress?.(event);
61
+ };
62
+
63
+ const spinnerProps = {
64
+ ...variantStyles.spinner?.[state],
65
+ ...variantStyles.spinner?.default,
66
+ };
67
+
68
+ return (
69
+ <Pressable
70
+ {...props}
71
+ onPress={handlePress}
72
+ onHoverIn={() => setIsHovered(true)}
73
+ onHoverOut={() => setIsHovered(false)}
74
+ disabled={props.isDisabled}
75
+ style={[
76
+ variantStyles.root?.default,
77
+ variantStyles.root?.[state],
78
+ props.style,
79
+ ]}
80
+ >
81
+ {organizedChildren}
82
+ {props.isLoading && <ActivityIndicator {...spinnerProps} />}
83
+ </Pressable>
84
+ );
85
+ }
@@ -1,14 +1,2 @@
1
- import { ButtonLabel } from "./components/button-label";
2
- import { ButtonRoot } from "./components/button-root";
3
- import { ButtonSpinner } from "./components/button-spinner";
4
-
5
- export const Button = {
6
- Root: ButtonRoot,
7
- Label: ButtonLabel,
8
- Spinner: ButtonSpinner,
9
- };
10
-
11
- export type { ButtonLabelProps } from "./components/button-label";
12
- export type { ButtonRootProps } from "./components/button-root";
13
- export type { ButtonSpinnerProps } from "./components/button-spinner";
1
+ export { Button, type ButtonProps } from "./button";
14
2
  export type { ButtonStyles } from "./types";
@@ -1,11 +1,15 @@
1
- import type { ButtonLabelProps } from "./components/button-label";
2
- import type { ButtonRootProps } from "./components/button-root";
3
- import type { ButtonSpinnerProps } from "./components/button-spinner";
1
+ import type {
2
+ ActivityIndicatorProps,
3
+ TextStyle,
4
+ ViewStyle,
5
+ } from "react-native";
6
+ import type { IconProps } from "../icon";
4
7
 
5
8
  export type ButtonState = "default" | "disabled" | "loading" | "hovered";
6
9
 
7
10
  export interface ButtonStyles {
8
- root?: Partial<Record<ButtonState, ButtonRootProps["style"]>>;
9
- label?: Partial<Record<ButtonState, ButtonLabelProps["style"]>>;
10
- spinner?: Partial<Record<ButtonState, ButtonSpinnerProps>>;
11
+ root?: Partial<Record<ButtonState, ViewStyle>>;
12
+ text?: Partial<Record<ButtonState, TextStyle>>;
13
+ icon?: Partial<Record<ButtonState, IconProps>>;
14
+ spinner?: Partial<Record<ButtonState, ActivityIndicatorProps>>;
11
15
  }
@@ -1,6 +1,7 @@
1
- import { type ButtonStyles } from "../..";
1
+ import type { CursorValue } from "react-native";
2
2
  import { hslaSetRelativeLightness } from "../../../utils/hsla-utils";
3
3
  import { useThemedStyles } from "../../../utils/use-themed-styles";
4
+ import type { ButtonStyles } from "../types";
4
5
 
5
6
  export const useButtonVariantDefault = (): ButtonStyles => {
6
7
  return useThemedStyles(
@@ -17,32 +18,37 @@ export const useButtonVariantDefault = (): ButtonStyles => {
17
18
  backgroundColor: colors.primary,
18
19
  borderWidth: 1,
19
20
  borderColor: colors.border,
21
+ cursor: "pointer",
20
22
  },
21
23
  disabled: {
22
24
  opacity: 0.5,
25
+ cursor: "not-allowed" as CursorValue,
23
26
  },
24
27
  loading: {
25
28
  opacity: 0.5,
29
+ cursor: "wait" as CursorValue,
26
30
  },
27
31
  hovered: {
28
32
  backgroundColor: hslaSetRelativeLightness(colors.primary, -10),
29
33
  },
30
34
  },
31
- label: {
35
+ text: {
32
36
  default: {
33
37
  color: colors.primaryForeground,
34
38
  fontSize,
35
39
  fontFamily,
36
40
  },
37
- disabled: {},
38
- loading: {},
41
+ },
42
+ icon: {
43
+ default: {
44
+ color: colors.primaryForeground,
45
+ size: fontSize,
46
+ },
39
47
  },
40
48
  spinner: {
41
49
  default: {
42
50
  color: colors.primaryForeground,
43
51
  },
44
- disabled: {},
45
- loading: {},
46
52
  },
47
53
  }),
48
54
  );
@@ -1,6 +1,7 @@
1
- import { type ButtonStyles } from "../..";
1
+ import type { CursorValue } from "react-native";
2
2
  import { hslaSetRelativeLightness } from "../../../utils/hsla-utils";
3
3
  import { useThemedStyles } from "../../../utils/use-themed-styles";
4
+ import type { ButtonStyles } from "../types";
4
5
 
5
6
  export const useButtonVariantGhost = (): ButtonStyles => {
6
7
  return useThemedStyles(
@@ -15,18 +16,21 @@ export const useButtonVariantGhost = (): ButtonStyles => {
15
16
  alignItems: "center",
16
17
  justifyContent: "center",
17
18
  backgroundColor: "transparent",
19
+ cursor: "pointer",
18
20
  },
19
21
  disabled: {
20
22
  opacity: 0.5,
23
+ cursor: "not-allowed" as CursorValue,
21
24
  },
22
25
  loading: {
23
26
  opacity: 0.5,
27
+ cursor: "wait" as CursorValue,
24
28
  },
25
29
  hovered: {
26
30
  backgroundColor: hslaSetRelativeLightness(colors.secondary, -1),
27
31
  },
28
32
  },
29
- label: {
33
+ text: {
30
34
  default: {
31
35
  color: colors.foreground,
32
36
  fontSize,
@@ -39,6 +43,18 @@ export const useButtonVariantGhost = (): ButtonStyles => {
39
43
  color: colors.mutedForeground,
40
44
  },
41
45
  },
46
+ icon: {
47
+ default: {
48
+ color: colors.foreground,
49
+ size: fontSize,
50
+ },
51
+ disabled: {
52
+ color: colors.mutedForeground,
53
+ },
54
+ loading: {
55
+ color: colors.mutedForeground,
56
+ },
57
+ },
42
58
  spinner: {
43
59
  default: {
44
60
  color: colors.foreground,
@@ -1,6 +1,7 @@
1
- import { type ButtonStyles } from "../..";
1
+ import type { CursorValue } from "react-native";
2
2
  import { hslaSetRelativeLightness } from "../../../utils/hsla-utils";
3
3
  import { useThemedStyles } from "../../../utils/use-themed-styles";
4
+ import type { ButtonStyles } from "../types";
4
5
 
5
6
  export const useButtonVariantSecondary = (): ButtonStyles => {
6
7
  return useThemedStyles(
@@ -17,18 +18,21 @@ export const useButtonVariantSecondary = (): ButtonStyles => {
17
18
  borderWidth: 1,
18
19
  borderColor: colors.border,
19
20
  backgroundColor: colors.secondary,
21
+ cursor: "pointer",
20
22
  },
21
23
  disabled: {
22
24
  opacity: 0.5,
25
+ cursor: "not-allowed" as CursorValue,
23
26
  },
24
27
  loading: {
25
28
  opacity: 0.5,
29
+ cursor: "wait" as CursorValue,
26
30
  },
27
31
  hovered: {
28
32
  backgroundColor: hslaSetRelativeLightness(colors.secondary, -1),
29
33
  },
30
34
  },
31
- label: {
35
+ text: {
32
36
  default: {
33
37
  color: colors.secondaryForeground,
34
38
  fontSize,
@@ -41,6 +45,18 @@ export const useButtonVariantSecondary = (): ButtonStyles => {
41
45
  color: colors.mutedForeground,
42
46
  },
43
47
  },
48
+ icon: {
49
+ default: {
50
+ color: colors.secondaryForeground,
51
+ size: fontSize,
52
+ },
53
+ disabled: {
54
+ color: colors.mutedForeground,
55
+ },
56
+ loading: {
57
+ color: colors.mutedForeground,
58
+ },
59
+ },
44
60
  spinner: {
45
61
  default: {
46
62
  color: colors.secondaryForeground,
@@ -0,0 +1,51 @@
1
+ import React from "react";
2
+ import {
3
+ ScrollView,
4
+ StyleSheet,
5
+ type StyleProp,
6
+ type ViewStyle,
7
+ } from "react-native";
8
+ import { useRelativePosition } from "../../../hooks/use-relative-position";
9
+ import { useCombobox } from "../context";
10
+
11
+ export interface ComboboxContentProps {
12
+ children?: React.ReactNode;
13
+ style?: StyleProp<ViewStyle>;
14
+ }
15
+
16
+ export function ComboboxContent(props: ComboboxContentProps) {
17
+ const combobox = useCombobox();
18
+ const composedStyles = StyleSheet.flatten([
19
+ combobox.styles?.content?.default,
20
+ combobox.styles?.content?.[combobox.state],
21
+ props.style,
22
+ ]);
23
+
24
+ const flatStyles = StyleSheet.flatten(composedStyles);
25
+
26
+ const positionStyle = useRelativePosition({
27
+ align: "start",
28
+ triggerPosition: combobox.triggerPosition,
29
+ contentLayout: combobox.contentLayout,
30
+ alignOffset: 0,
31
+ preferredSide: "bottom",
32
+ sideOffset: 2,
33
+ });
34
+
35
+ return (
36
+ <ScrollView
37
+ style={[
38
+ positionStyle,
39
+ flatStyles,
40
+ { width: combobox.triggerPosition.width },
41
+ ]}
42
+ onLayout={(e) => {
43
+ combobox.setContentLayout(e.nativeEvent.layout);
44
+ }}
45
+ keyboardShouldPersistTaps="handled"
46
+ nestedScrollEnabled
47
+ >
48
+ {props.children}
49
+ </ScrollView>
50
+ );
51
+ }
@@ -0,0 +1,28 @@
1
+ import React from "react";
2
+ import { Text } from "react-native";
3
+ import { calculateComposedStyles } from "../../../utils/calculate-styles";
4
+ import { useCombobox } from "../context";
5
+
6
+ export interface ComboboxEmptyProps {
7
+ children?: React.ReactNode;
8
+ }
9
+
10
+ export function ComboboxEmpty(props: ComboboxEmptyProps) {
11
+ const combobox = useCombobox();
12
+
13
+ const hasVisibleOptions = combobox.options.some((option) =>
14
+ combobox.filter(option.value, combobox.searchQuery),
15
+ );
16
+
17
+ if (hasVisibleOptions) {
18
+ return null;
19
+ }
20
+
21
+ const composedStyles = calculateComposedStyles(
22
+ combobox.styles,
23
+ combobox.state,
24
+ "empty",
25
+ );
26
+
27
+ return <Text style={composedStyles}>{props.children}</Text>;
28
+ }
@@ -0,0 +1,81 @@
1
+ import React, { useEffect, useState } from "react";
2
+ import { Pressable, StyleSheet, Text } from "react-native";
3
+ import { useCombobox } from "../context";
4
+ import type { ComboboxOptionState, ComboboxState } from "../types";
5
+
6
+ export type ComboboxOptionProps = {
7
+ value: string;
8
+ label?: string;
9
+ children?: React.ReactNode;
10
+ };
11
+
12
+ const calculateState = (
13
+ comboboxState: ComboboxState,
14
+ hovered: boolean,
15
+ selected: boolean,
16
+ ): ComboboxOptionState => {
17
+ if (comboboxState === "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 ComboboxOption(
30
+ props: ComboboxOptionProps,
31
+ ): React.ReactElement | null {
32
+ const [isHovered, setIsHovered] = useState(false);
33
+ const combobox = useCombobox();
34
+ const isSelected = combobox.value === props.value;
35
+
36
+ const optionState = calculateState(combobox.state, isHovered, isSelected);
37
+ const composedStyles = StyleSheet.flatten([
38
+ combobox.styles?.option?.default,
39
+ combobox.styles?.option?.[optionState],
40
+ ]);
41
+
42
+ useEffect(() => {
43
+ combobox.setOptions((prev) => {
44
+ if (prev.find((option) => option.value === props.value)) {
45
+ return prev;
46
+ }
47
+ return [
48
+ ...prev,
49
+ {
50
+ value: props.value,
51
+ label: props.label ?? props.children,
52
+ },
53
+ ];
54
+ });
55
+ }, [props.value, props.label, props.children]);
56
+
57
+ if (!combobox.filter(props.value, combobox.searchQuery)) {
58
+ return null;
59
+ }
60
+
61
+ const Component = typeof props.children === "string" ? Text : Pressable;
62
+
63
+ return (
64
+ <Component
65
+ onPress={() => {
66
+ combobox.onChange?.(props.value);
67
+ combobox.setIsOpen(false);
68
+ combobox.setSearchQuery("");
69
+ }}
70
+ onPointerEnter={() => {
71
+ setIsHovered(true);
72
+ }}
73
+ onPointerLeave={() => {
74
+ setIsHovered(false);
75
+ }}
76
+ style={composedStyles}
77
+ >
78
+ {props.children}
79
+ </Component>
80
+ );
81
+ }
@@ -0,0 +1,36 @@
1
+ import React from "react";
2
+ import {
3
+ Pressable,
4
+ type StyleProp,
5
+ StyleSheet,
6
+ type ViewStyle,
7
+ } from "react-native";
8
+ import { useCombobox } from "../context";
9
+
10
+ export interface ComboboxOverlayProps {
11
+ children?: React.ReactNode;
12
+ style?: StyleProp<ViewStyle>;
13
+ }
14
+
15
+ export function ComboboxOverlay(props: ComboboxOverlayProps) {
16
+ const combobox = useCombobox();
17
+
18
+ const composedStyles = StyleSheet.flatten([
19
+ combobox.styles?.overlay?.default,
20
+ combobox.styles?.overlay?.[combobox.state],
21
+ props.style,
22
+ ]);
23
+
24
+ return (
25
+ <Pressable
26
+ onPress={() => {
27
+ combobox.setIsOpen(false);
28
+ combobox.setSearchQuery("");
29
+ }}
30
+ pointerEvents="auto"
31
+ style={[StyleSheet.absoluteFill, composedStyles]}
32
+ >
33
+ {props.children}
34
+ </Pressable>
35
+ );
36
+ }
@@ -0,0 +1,33 @@
1
+ import React from "react";
2
+ import { View } from "react-native";
3
+ import { Portal } from "../../portal";
4
+ import { ComboboxContext, useCombobox } from "../context";
5
+
6
+ export interface ComboboxPortalProps {
7
+ children?: React.ReactNode;
8
+ }
9
+
10
+ export function ComboboxPortal(props: ComboboxPortalProps) {
11
+ const combobox = useCombobox();
12
+
13
+ if (!combobox.isOpen) {
14
+ return (
15
+ <View
16
+ style={{ display: "none" }}
17
+ aria-hidden
18
+ accessibilityElementsHidden
19
+ importantForAccessibility="no-hide-descendants"
20
+ >
21
+ {props.children}
22
+ </View>
23
+ );
24
+ }
25
+
26
+ return (
27
+ <Portal name="combobox-portal">
28
+ <ComboboxContext.Provider value={combobox}>
29
+ {props.children}
30
+ </ComboboxContext.Provider>
31
+ </Portal>
32
+ );
33
+ }
@@ -0,0 +1,152 @@
1
+ import React, { useCallback, useMemo, useRef, useState } from "react";
2
+ import {
3
+ type LayoutRectangle,
4
+ type StyleProp,
5
+ StyleSheet,
6
+ View,
7
+ type ViewStyle,
8
+ } from "react-native";
9
+ import {
10
+ DEFAULT_LAYOUT,
11
+ DEFAULT_POSITION,
12
+ type LayoutPosition,
13
+ } from "../../../hooks";
14
+ import { ComboboxContext } from "../context";
15
+ import type { ComboboxOption, ComboboxState } from "../types";
16
+ import { ComboboxVariants } from "../variants";
17
+
18
+ export interface ComboboxRootProps {
19
+ variant?: keyof typeof ComboboxVariants;
20
+
21
+ value?: string;
22
+ onChange?: (value: string) => void;
23
+
24
+ /** Called when the search input text changes. Use for async/remote filtering. */
25
+ onSearchChange?: (query: string) => void;
26
+
27
+ /** Custom filter function. Receives option value and search query, returns whether to show the option.
28
+ * Default: case-insensitive match against option label/keywords.
29
+ * For async/remote filtering, pass `() => true` to show all options. */
30
+ filter?: (value: string, query: string) => boolean;
31
+
32
+ isDisabled?: boolean;
33
+
34
+ children?: React.ReactNode;
35
+ style?: StyleProp<ViewStyle>;
36
+ }
37
+
38
+ const calculateState = (props: ComboboxRootProps): ComboboxState => {
39
+ if (props.isDisabled) {
40
+ return "disabled";
41
+ }
42
+ return "default";
43
+ };
44
+
45
+ const defaultFilter = (
46
+ value: string,
47
+ query: string,
48
+ options: Array<ComboboxOption>,
49
+ ): boolean => {
50
+ if (!query) return true;
51
+ const lowerQuery = query.toLowerCase();
52
+ const option = options.find((o) => o.value === value);
53
+ if (!option) return true;
54
+
55
+ if (option.keywords) {
56
+ return option.keywords.some((kw) => kw.toLowerCase().includes(lowerQuery));
57
+ }
58
+
59
+ if (typeof option.label === "string") {
60
+ return option.label.toLowerCase().includes(lowerQuery);
61
+ }
62
+
63
+ return true;
64
+ };
65
+
66
+ export function ComboboxRoot(props: ComboboxRootProps) {
67
+ const variantStyles = ComboboxVariants[props.variant ?? "default"]();
68
+
69
+ const [isOpen, setIsOpen] = useState(false);
70
+ const [contentLayout, setContentLayout] =
71
+ useState<LayoutRectangle>(DEFAULT_LAYOUT);
72
+ const [triggerPosition, setTriggerPosition] =
73
+ useState<LayoutPosition>(DEFAULT_POSITION);
74
+ const [options, setOptions] = useState<Array<ComboboxOption>>([]);
75
+ const [searchQuery, setSearchQueryInternal] = useState("");
76
+
77
+ const onSearchChangeRef = useRef(props.onSearchChange);
78
+ onSearchChangeRef.current = props.onSearchChange;
79
+
80
+ const setSearchQuery: React.Dispatch<React.SetStateAction<string>> =
81
+ useCallback((action) => {
82
+ setSearchQueryInternal((prev) => {
83
+ const next = typeof action === "function" ? action(prev) : action;
84
+ if (next !== prev) {
85
+ onSearchChangeRef.current?.(next);
86
+ }
87
+ return next;
88
+ });
89
+ }, []);
90
+
91
+ const filterRef = useRef(props.filter);
92
+ filterRef.current = props.filter;
93
+
94
+ const optionsRef = useRef(options);
95
+ optionsRef.current = options;
96
+
97
+ const filter = useCallback(
98
+ (value: string, query: string) =>
99
+ filterRef.current
100
+ ? filterRef.current(value, query)
101
+ : defaultFilter(value, query, optionsRef.current),
102
+ [],
103
+ );
104
+
105
+ const state = calculateState(props);
106
+ const composedStyles = StyleSheet.flatten([
107
+ variantStyles?.root?.default,
108
+ variantStyles?.root?.[state],
109
+ props.style,
110
+ ]);
111
+
112
+ const contextValue: ComboboxContext = useMemo(
113
+ () => ({
114
+ value: props.value,
115
+ onChange: props.onChange,
116
+ isOpen,
117
+ setIsOpen,
118
+ triggerPosition,
119
+ setTriggerPosition,
120
+ contentLayout,
121
+ setContentLayout,
122
+ options,
123
+ setOptions,
124
+ searchQuery,
125
+ setSearchQuery,
126
+ filter,
127
+ state,
128
+ isDisabled: props.isDisabled ?? false,
129
+ styles: variantStyles,
130
+ }),
131
+ [
132
+ props.value,
133
+ props.onChange,
134
+ isOpen,
135
+ triggerPosition,
136
+ contentLayout,
137
+ options,
138
+ searchQuery,
139
+ setSearchQuery,
140
+ filter,
141
+ state,
142
+ props.isDisabled,
143
+ variantStyles,
144
+ ],
145
+ );
146
+
147
+ return (
148
+ <ComboboxContext.Provider value={contextValue}>
149
+ <View style={composedStyles}>{props.children}</View>
150
+ </ComboboxContext.Provider>
151
+ );
152
+ }