@vellira-ui/react-native 2.55.0 → 2.57.0

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 (2) hide show
  1. package/dist/index.js +222 -142
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { Children, cloneElement, createContext, forwardRef, isValidElement, useCallback, useContext, useEffect, useId, useMemo, useRef, useState } from "react";
2
2
  import { Check, ChevronDown, Close, Search } from "@vellira-ui/icons";
3
- import { AccessibilityInfo, ActivityIndicator, Animated, Dimensions, Easing, FlatList, Modal as Modal$1, Platform, Pressable, ScrollView, StyleSheet, Text, TextInput, View, findNodeHandle, useWindowDimensions } from "react-native";
3
+ import { AccessibilityInfo, ActivityIndicator, Animated, BackHandler, Dimensions, Easing, FlatList, Modal as Modal$1, Platform, Pressable, ScrollView, StyleSheet, Text, TextInput, View, findNodeHandle, useWindowDimensions } from "react-native";
4
4
  import { darkTheme, highContrastTheme, lightTheme } from "@vellira-ui/tokens";
5
5
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
6
6
  //#region src/theme/fontWeight.ts
@@ -205,41 +205,59 @@ function useNativeFloatingPosition(placement = "top", offset = 8) {
205
205
  };
206
206
  }
207
207
  //#endregion
208
- //#region src/managers/OverlayStack/NativeOverlayStack.ts
208
+ //#region src/managers/OverlayManager/NativeOverlayManager.ts
209
+ const BASE_LAYER = 1e3;
210
+ const LAYER_STEP = 10;
209
211
  let stack = [];
210
- const nativeOverlayStackStore = {
211
- add(id) {
212
- stack = stack.filter((item) => item !== id);
213
- stack.push(id);
212
+ const nativeOverlayManager = {
213
+ register(id) {
214
+ stack = stack.filter((item) => item.id !== id);
215
+ const entry = {
216
+ id,
217
+ layer: BASE_LAYER + stack.length * LAYER_STEP
218
+ };
219
+ stack.push(entry);
220
+ return entry;
214
221
  },
215
- remove(id) {
216
- stack = stack.filter((item) => item !== id);
222
+ unregister(id) {
223
+ stack = stack.filter((item) => item.id !== id);
217
224
  },
218
225
  isTop(id) {
219
- return stack[stack.length - 1] === id;
226
+ return stack.at(-1)?.id === id;
227
+ },
228
+ getTop() {
229
+ return stack.at(-1);
230
+ },
231
+ getLayer(id) {
232
+ return stack.find((item) => item.id === id)?.layer ?? BASE_LAYER;
220
233
  }
221
234
  };
222
235
  //#endregion
223
- //#region src/managers/OverlayStack/useNativeOverlayStack.ts
224
- const useNativeOverlayStack = ({ id, visible }) => {
236
+ //#region src/managers/OverlayManager/useNativeOverlayRegistration.ts
237
+ const useNativeOverlayRegistration = ({ id, visible }) => {
238
+ const [layer, setLayer] = useState(() => nativeOverlayManager.getLayer(id));
225
239
  useEffect(() => {
226
240
  if (!visible) return;
227
- nativeOverlayStackStore.add(id);
241
+ const entry = nativeOverlayManager.register(id);
242
+ setLayer(entry.layer);
228
243
  return () => {
229
- nativeOverlayStackStore.remove(id);
244
+ nativeOverlayManager.unregister(id);
230
245
  };
231
246
  }, [id, visible]);
232
- return { isTopOverlay: useCallback(() => nativeOverlayStackStore.isTop(id), [id]) };
247
+ return {
248
+ layer,
249
+ isTopOverlay: useCallback(() => nativeOverlayManager.isTop(id), [id])
250
+ };
233
251
  };
234
252
  //#endregion
235
253
  //#region src/hooks/behavior/overlay/useOverlayStack.ts
236
- const useOverlayStack = ({ active, id }) => useNativeOverlayStack({
254
+ const useOverlayStack = ({ active, id }) => useNativeOverlayRegistration({
237
255
  id,
238
256
  visible: active
239
257
  });
240
258
  //#endregion
241
259
  //#region src/hooks/behavior/overlay/useOverlayDismiss.ts
242
- const useOverlayDismiss = ({ active, closeOnOutsidePress = true, id, requestClose }) => {
260
+ const useOverlayDismiss = ({ active, closeOnEscape = true, closeOnOutsidePress = true, id, requestClose }) => {
243
261
  const { isTopOverlay } = useOverlayStack({
244
262
  active,
245
263
  id
@@ -248,16 +266,73 @@ const useOverlayDismiss = ({ active, closeOnOutsidePress = true, id, requestClos
248
266
  if (!isTopOverlay()) return;
249
267
  requestClose();
250
268
  }, [isTopOverlay, requestClose]);
269
+ const requestOutsideClose = useCallback(() => {
270
+ if (!closeOnOutsidePress) return;
271
+ requestTopClose();
272
+ }, [closeOnOutsidePress, requestTopClose]);
273
+ useEffect(() => {
274
+ if (!active || !closeOnEscape) return;
275
+ if (Platform.OS === "web") {
276
+ const handleKeyDown = (event) => {
277
+ if (event.key !== "Escape") return;
278
+ requestTopClose();
279
+ };
280
+ document.addEventListener("keydown", handleKeyDown);
281
+ return () => {
282
+ document.removeEventListener("keydown", handleKeyDown);
283
+ };
284
+ }
285
+ const subscription = BackHandler.addEventListener("hardwareBackPress", () => {
286
+ if (!isTopOverlay()) return false;
287
+ requestClose();
288
+ return true;
289
+ });
290
+ return () => {
291
+ subscription.remove();
292
+ };
293
+ }, [
294
+ active,
295
+ closeOnEscape,
296
+ isTopOverlay,
297
+ requestClose,
298
+ requestTopClose
299
+ ]);
251
300
  return {
252
301
  isTopOverlay,
253
302
  requestClose: requestTopClose,
254
- requestOutsideClose: useCallback(() => {
255
- if (!closeOnOutsidePress) return;
256
- requestTopClose();
257
- }, [closeOnOutsidePress, requestTopClose])
303
+ requestOutsideClose
258
304
  };
259
305
  };
260
306
  //#endregion
307
+ //#region src/hooks/behavior/overlay/useOverlayFocusRestore.ts
308
+ const useOverlayFocusRestore = ({ enabled = true, triggerRef }) => {
309
+ const restoreFocus = useCallback(() => {
310
+ if (!enabled) return;
311
+ if (Platform.OS === "web") {
312
+ const triggerNode = triggerRef.current;
313
+ if (triggerNode && typeof triggerNode === "object" && "focus" in triggerNode && typeof triggerNode.focus === "function") triggerNode.focus();
314
+ return;
315
+ }
316
+ if (typeof findNodeHandle !== "function") return;
317
+ const handle = findNodeHandle(triggerRef.current);
318
+ if (handle && AccessibilityInfo.setAccessibilityFocus) AccessibilityInfo.setAccessibilityFocus(handle);
319
+ }, [enabled, triggerRef]);
320
+ return {
321
+ restoreFocus,
322
+ restoreFocusAfterClose: useCallback(() => {
323
+ if (!enabled) return;
324
+ requestAnimationFrame(restoreFocus);
325
+ }, [enabled, restoreFocus])
326
+ };
327
+ };
328
+ //#endregion
329
+ //#region src/hooks/behavior/overlay/useOverlayPresentation.ts
330
+ function useOverlayPresentation(presentation = "auto", breakpoint = 768) {
331
+ const { width } = useWindowDimensions();
332
+ if (presentation === "auto") return width >= breakpoint ? "popover" : "sheet";
333
+ return presentation;
334
+ }
335
+ //#endregion
261
336
  //#region src/hooks/useControllableState.ts
262
337
  const useControllableState = ({ value, defaultValue, onChange }) => {
263
338
  const [internalValue, setInternalValue] = useState(defaultValue);
@@ -698,11 +773,7 @@ const createStyles$21 = (theme) => StyleSheet.create({
698
773
  justifyContent: "center",
699
774
  padding: theme.tokens.spacing[4]
700
775
  },
701
- popover: {
702
- alignItems: "center",
703
- justifyContent: "center",
704
- padding: theme.tokens.spacing[4]
705
- },
776
+ popover: { flex: 1 },
706
777
  backdrop: {
707
778
  ...StyleSheet.absoluteFill,
708
779
  backgroundColor: theme.semantic.overlay.backdrop
@@ -777,11 +848,12 @@ const createStyles$21 = (theme) => StyleSheet.create({
777
848
  //#region src/components/Dropdown/Content/DropdownContent.tsx
778
849
  const nativePointerEventsBoxNone$1 = Platform.OS === "web" ? void 0 : { pointerEvents: "box-none" };
779
850
  const webPointerEventsBoxNone$1 = Platform.OS === "web" ? { pointerEvents: "box-none" } : void 0;
780
- function DropdownContent({ isOpen, children, onClose, color = "primary", contentStyle, accessibilityLabel, presentation, searchable = false, searchValue = "", searchPlaceholder = "Search actions...", searchAccessibilityLabel, onSearchChange }) {
851
+ function DropdownContent({ isOpen, children, onClose, color = "primary", contentStyle, accessibilityLabel, presentation, position, onFloatingLayout, searchable = false, searchValue = "", searchPlaceholder = "Search actions...", searchAccessibilityLabel, onSearchChange }) {
781
852
  const { theme } = useTheme();
782
853
  const styles = useThemeStyles(createStyles$21);
783
854
  const colorPalette = theme.components.dropdown[color];
784
855
  const isSheet = presentation === "sheet";
856
+ const isPopover = presentation === "popover";
785
857
  const animation = useRef(new Animated.Value(isOpen ? 1 : 0)).current;
786
858
  const [reduceMotion, setReduceMotion] = useState(false);
787
859
  useEffect(() => {
@@ -857,9 +929,15 @@ function DropdownContent({ isOpen, children, onClose, color = "primary", content
857
929
  styles.menu,
858
930
  styles[`${presentation}Menu`],
859
931
  { borderColor: colorPalette.content.border },
932
+ isPopover && {
933
+ position: "absolute",
934
+ top: position.top,
935
+ left: position.left
936
+ },
860
937
  contentStyle,
861
938
  menuAnimatedStyle
862
939
  ],
940
+ onLayout: isPopover ? onFloatingLayout : void 0,
863
941
  children: [searchable && /* @__PURE__ */ jsxs(View, {
864
942
  style: styles.searchWrap,
865
943
  children: [/* @__PURE__ */ jsx(View, {
@@ -887,27 +965,6 @@ function DropdownContent({ isOpen, children, onClose, color = "primary", content
887
965
  }
888
966
  DropdownContent.displayName = "DropdownContent";
889
967
  //#endregion
890
- //#region src/components/Dropdown/Group/DropdownGroup.styles.ts
891
- const createStyles$20 = (theme) => StyleSheet.create({ groupLabel: {
892
- paddingHorizontal: theme.tokens.spacing[4],
893
- paddingVertical: theme.tokens.spacing[2],
894
- color: theme.components.dropdown.groupLabel.fg,
895
- fontFamily: theme.tokens.typography.family.medium,
896
- fontSize: theme.tokens.typography.size.xs,
897
- lineHeight: theme.tokens.typography.lineHeight.sm,
898
- textTransform: "uppercase"
899
- } });
900
- //#endregion
901
- //#region src/components/Dropdown/Group/DropdownGroup.tsx
902
- function DropdownGroup({ label }) {
903
- return /* @__PURE__ */ jsx(Text, {
904
- accessibilityRole: "header",
905
- style: useThemeStyles(createStyles$20).groupLabel,
906
- children: label
907
- });
908
- }
909
- DropdownGroup.displayName = "DropdownGroup";
910
- //#endregion
911
968
  //#region src/components/Dropdown/internal/DropdownCollection.ts
912
969
  function createDropdownSlot(name, displayName) {
913
970
  const Slot = () => null;
@@ -1011,8 +1068,65 @@ function getItemLabel(children) {
1011
1068
  return labelParts.join("").trim();
1012
1069
  }
1013
1070
  //#endregion
1071
+ //#region src/components/Dropdown/Dropdown.styles.ts
1072
+ const createStyles$20 = (theme) => StyleSheet.create({
1073
+ root: { alignSelf: "flex-start" },
1074
+ emptyText: {
1075
+ minHeight: 44,
1076
+ paddingHorizontal: theme.tokens.spacing[3],
1077
+ paddingVertical: theme.tokens.spacing[3],
1078
+ color: theme.components.dropdown.item.disabled.fg,
1079
+ fontFamily: theme.tokens.typography.family.regular,
1080
+ fontSize: theme.tokens.typography.size.md,
1081
+ lineHeight: theme.tokens.typography.lineHeight.md
1082
+ }
1083
+ });
1084
+ //#endregion
1085
+ //#region src/components/Dropdown/Group/DropdownGroup.styles.ts
1086
+ const createStyles$19 = (theme) => StyleSheet.create({ groupLabel: {
1087
+ paddingHorizontal: theme.tokens.spacing[4],
1088
+ paddingVertical: theme.tokens.spacing[2],
1089
+ color: theme.components.dropdown.groupLabel.fg,
1090
+ fontFamily: theme.tokens.typography.family.medium,
1091
+ fontSize: theme.tokens.typography.size.xs,
1092
+ lineHeight: theme.tokens.typography.lineHeight.sm,
1093
+ textTransform: "uppercase"
1094
+ } });
1095
+ //#endregion
1096
+ //#region src/components/Dropdown/Group/DropdownGroup.tsx
1097
+ function DropdownGroup({ label }) {
1098
+ return /* @__PURE__ */ jsx(Text, {
1099
+ accessibilityRole: "header",
1100
+ style: useThemeStyles(createStyles$19).groupLabel,
1101
+ children: label
1102
+ });
1103
+ }
1104
+ DropdownGroup.displayName = "DropdownGroup";
1105
+ //#endregion
1106
+ //#region src/components/Dropdown/internal/DropdownUtils.ts
1107
+ function createDropdownSelectEvent() {
1108
+ let defaultPrevented = false;
1109
+ return {
1110
+ preventDefault: () => {
1111
+ defaultPrevented = true;
1112
+ },
1113
+ get defaultPrevented() {
1114
+ return defaultPrevented;
1115
+ }
1116
+ };
1117
+ }
1118
+ function filterDropdownEntries(parsed, searchValue) {
1119
+ const normalizedSearch = searchValue.trim().toLocaleLowerCase();
1120
+ const matchedItems = new Set(parsed.items.filter((item) => item.label.toLocaleLowerCase().includes(normalizedSearch)).map((item) => item.id));
1121
+ return {
1122
+ ...parsed,
1123
+ items: parsed.items.filter((item) => matchedItems.has(item.id)),
1124
+ entries: parsed.entries.filter((entry) => entry.type !== "item" || matchedItems.has(entry.id))
1125
+ };
1126
+ }
1127
+ //#endregion
1014
1128
  //#region src/components/Dropdown/Item/DropdownItem.styles.ts
1015
- const createStyles$19 = (theme) => StyleSheet.create({
1129
+ const createStyles$18 = (theme) => StyleSheet.create({
1016
1130
  item: {
1017
1131
  minWidth: 0,
1018
1132
  minHeight: 44,
@@ -1037,7 +1151,7 @@ const createStyles$19 = (theme) => StyleSheet.create({
1037
1151
  //#region src/components/Dropdown/Item/DropdownItem.tsx
1038
1152
  function DropdownItem({ label, value, rootColor = "primary", color = "default", icon, disabled = false, textWrap = "truncate", itemStyle, textStyle, onSelect }) {
1039
1153
  const { theme } = useTheme();
1040
- const styles = useThemeStyles(createStyles$19);
1154
+ const styles = useThemeStyles(createStyles$18);
1041
1155
  const rootColorPalette = theme.components.dropdown[rootColor];
1042
1156
  const itemColorPalette = color === "default" ? void 0 : theme.components.dropdown[color];
1043
1157
  const renderColoredNode = (node, color) => {
@@ -1091,19 +1205,19 @@ function DropdownItem({ label, value, rootColor = "primary", color = "default",
1091
1205
  DropdownItem.displayName = "DropdownItem";
1092
1206
  //#endregion
1093
1207
  //#region src/components/Dropdown/Separator/DropdownSeparator.styles.ts
1094
- const createStyles$18 = (theme) => StyleSheet.create({ separator: {
1208
+ const createStyles$17 = (theme) => StyleSheet.create({ separator: {
1095
1209
  height: 1,
1096
1210
  backgroundColor: theme.components.dropdown.separator.bg
1097
1211
  } });
1098
1212
  //#endregion
1099
1213
  //#region src/components/Dropdown/Separator/DropdownSeparator.tsx
1100
1214
  function DropdownSeparator() {
1101
- return /* @__PURE__ */ jsx(View, { style: useThemeStyles(createStyles$18).separator });
1215
+ return /* @__PURE__ */ jsx(View, { style: useThemeStyles(createStyles$17).separator });
1102
1216
  }
1103
1217
  DropdownSeparator.displayName = "DropdownSeparator";
1104
1218
  //#endregion
1105
1219
  //#region src/components/Dropdown/Trigger/DropdownTrigger.styles.ts
1106
- const createStyles$17 = (theme) => StyleSheet.create({
1220
+ const createStyles$16 = (theme) => StyleSheet.create({
1107
1221
  trigger: {
1108
1222
  alignItems: "center",
1109
1223
  justifyContent: "center",
@@ -1191,7 +1305,7 @@ const createStyles$17 = (theme) => StyleSheet.create({
1191
1305
  //#region src/components/Dropdown/Trigger/DropdownTrigger.tsx
1192
1306
  function DropdownTrigger({ asChild = false, label, trigger, children, icon, arrowIcon, showArrow = true, color = "primary", size = "md", disabled = false, isOpen, triggerStyle, triggerRef, accessibilityLabel, accessibilityHint, onPress }) {
1193
1307
  const { theme } = useTheme();
1194
- const styles = useThemeStyles(createStyles$17);
1308
+ const styles = useThemeStyles(createStyles$16);
1195
1309
  const colorPalette = theme.components.dropdown[color];
1196
1310
  const textSizeStyle = {
1197
1311
  sm: styles.textSm,
@@ -1311,32 +1425,27 @@ function DropdownTrigger({ asChild = false, label, trigger, children, icon, arro
1311
1425
  }
1312
1426
  DropdownTrigger.displayName = "DropdownTrigger";
1313
1427
  //#endregion
1314
- //#region src/components/Dropdown/Dropdown.styles.ts
1315
- const createStyles$16 = (theme) => StyleSheet.create({
1316
- root: { alignSelf: "flex-start" },
1317
- emptyText: {
1318
- minHeight: 44,
1319
- paddingHorizontal: theme.tokens.spacing[3],
1320
- paddingVertical: theme.tokens.spacing[3],
1321
- color: theme.components.dropdown.item.disabled.fg,
1322
- fontFamily: theme.tokens.typography.family.regular,
1323
- fontSize: theme.tokens.typography.size.md,
1324
- lineHeight: theme.tokens.typography.lineHeight.md
1325
- }
1326
- });
1327
- //#endregion
1328
- //#region src/components/Dropdown/Dropdown.tsx
1329
- function DropdownRoot({ children, label = "Menu", trigger, icon, arrowIcon, showArrow = true, open, defaultOpen = false, onOpenChange, presentation = "auto", closeOnSelect = true, color = "primary", disabled = false, loading = false, loadingText = "Loading actions...", searchable = false, command = false, searchValue, defaultSearchValue = "", searchPlaceholder, onSearch, empty, size = "md", style, triggerStyle, contentStyle, itemStyle, textStyle, accessibilityLabel, accessibilityHint }) {
1330
- const styles = useThemeStyles(createStyles$16);
1428
+ //#region src/components/Dropdown/Root/DropdownRoot.tsx
1429
+ function DropdownRoot({ children, label = "Menu", trigger, icon, arrowIcon, showArrow = true, open, defaultOpen = false, onOpenChange, presentation = "auto", placement = "bottom-start", offset = 8, closeOnSelect = true, color = "primary", disabled = false, loading = false, loadingText = "Loading actions...", searchable = false, command = false, searchValue, defaultSearchValue = "", searchPlaceholder, onSearch, empty, size = "md", style, triggerStyle, contentStyle, itemStyle, textStyle, accessibilityLabel, accessibilityHint }) {
1430
+ const styles = useThemeStyles(createStyles$20);
1331
1431
  const overlayId = useId();
1332
1432
  const [uncontrolledSearchValue, setUncontrolledSearchValue] = useState(defaultSearchValue);
1333
1433
  const resolvedSearchValue = searchValue ?? uncontrolledSearchValue;
1334
- const { width } = useWindowDimensions();
1335
1434
  const triggerRef = useRef(null);
1435
+ const setTriggerRef = useCallback((node) => {
1436
+ if (node && typeof node === "object" && "measureInWindow" in node && typeof node.measureInWindow === "function") {
1437
+ triggerRef.current = node;
1438
+ return;
1439
+ }
1440
+ triggerRef.current = null;
1441
+ }, []);
1336
1442
  const parsed = useMemo(() => parseDropdownChildren(children), [children]);
1337
1443
  const contentCommand = parsed.contentProps?.command ?? false;
1338
1444
  const isSearchable = searchable || command || contentCommand || !!parsed.searchProps;
1339
- const resolvedPresentation = presentation === "auto" ? width < 768 ? "sheet" : "popover" : presentation;
1445
+ const resolvedPresentation = useOverlayPresentation(presentation);
1446
+ const contentStyleFromSlot = parsed.contentProps?.style;
1447
+ const contentPresentation = (parsed.contentProps?.presentation === "auto" ? void 0 : parsed.contentProps?.presentation) ?? resolvedPresentation;
1448
+ const { position, updatePosition, onFloatingLayout } = useNativeFloatingPosition(placement, offset);
1340
1449
  const menuAccessibilityLabel = useMemo(() => {
1341
1450
  if (accessibilityLabel) return accessibilityLabel;
1342
1451
  return typeof label === "string" ? label : "Menu";
@@ -1363,24 +1472,23 @@ function DropdownRoot({ children, label = "Menu", trigger, icon, arrowIcon, show
1363
1472
  getItemValue: (item) => item.value,
1364
1473
  getItemText: (item) => typeof item.label === "string" ? item.label : item.value
1365
1474
  });
1475
+ useEffect(() => {
1476
+ if (!isOpen || contentPresentation !== "popover") return;
1477
+ updatePosition(triggerRef);
1478
+ }, [
1479
+ isOpen,
1480
+ contentPresentation,
1481
+ updatePosition
1482
+ ]);
1366
1483
  useEffect(() => {
1367
1484
  if (!isOpen) return;
1368
1485
  AccessibilityInfo.announceForAccessibility(`${menuAccessibilityLabel} opened`);
1369
1486
  }, [isOpen, menuAccessibilityLabel]);
1370
- const focusTrigger = useCallback(() => {
1371
- if (Platform.OS === "web") {
1372
- const triggerNode = triggerRef.current;
1373
- if (triggerNode && typeof triggerNode === "object" && "focus" in triggerNode && typeof triggerNode.focus === "function") triggerNode.focus();
1374
- return;
1375
- }
1376
- if (typeof findNodeHandle !== "function") return;
1377
- const handle = findNodeHandle(triggerRef.current);
1378
- if (handle && AccessibilityInfo.setAccessibilityFocus) AccessibilityInfo.setAccessibilityFocus(handle);
1379
- }, []);
1487
+ const { restoreFocusAfterClose } = useOverlayFocusRestore({ triggerRef });
1380
1488
  const closeAndFocusTrigger = useCallback(() => {
1381
1489
  closeDropdown();
1382
- requestAnimationFrame(focusTrigger);
1383
- }, [closeDropdown, focusTrigger]);
1490
+ restoreFocusAfterClose();
1491
+ }, [closeDropdown, restoreFocusAfterClose]);
1384
1492
  const dismiss = useOverlayDismiss({
1385
1493
  id: overlayId,
1386
1494
  active: isOpen,
@@ -1443,8 +1551,6 @@ function DropdownRoot({ children, label = "Menu", trigger, icon, arrowIcon, show
1443
1551
  id: "empty",
1444
1552
  props: { children: empty ?? "No actions found" }
1445
1553
  }] : filteredParsed.entries;
1446
- const contentStyleFromSlot = parsed.contentProps?.style;
1447
- const presentationFromSlot = parsed.contentProps?.presentation === "auto" ? void 0 : parsed.contentProps?.presentation;
1448
1554
  return /* @__PURE__ */ jsxs(View, {
1449
1555
  style: [styles.root, style],
1450
1556
  children: [/* @__PURE__ */ jsx(DropdownTrigger, {
@@ -1458,9 +1564,7 @@ function DropdownRoot({ children, label = "Menu", trigger, icon, arrowIcon, show
1458
1564
  disabled: disabled || parsed.triggerProps?.disabled,
1459
1565
  isOpen,
1460
1566
  size,
1461
- triggerRef: (node) => {
1462
- triggerRef.current = node;
1463
- },
1567
+ triggerRef: setTriggerRef,
1464
1568
  triggerStyle,
1465
1569
  accessibilityLabel,
1466
1570
  accessibilityHint,
@@ -1471,7 +1575,9 @@ function DropdownRoot({ children, label = "Menu", trigger, icon, arrowIcon, show
1471
1575
  color,
1472
1576
  contentStyle: [contentStyle, contentStyleFromSlot],
1473
1577
  accessibilityLabel: menuAccessibilityLabel,
1474
- presentation: presentationFromSlot ?? resolvedPresentation,
1578
+ presentation: contentPresentation,
1579
+ position,
1580
+ onFloatingLayout,
1475
1581
  searchable: isSearchable,
1476
1582
  searchValue: resolvedSearchValue,
1477
1583
  searchPlaceholder: parsed.searchProps?.placeholder ?? searchPlaceholder ?? (command || contentCommand ? "Type a command..." : "Search actions..."),
@@ -1488,26 +1594,8 @@ function DropdownRoot({ children, label = "Menu", trigger, icon, arrowIcon, show
1488
1594
  });
1489
1595
  }
1490
1596
  DropdownRoot.displayName = "Dropdown";
1491
- function createDropdownSelectEvent() {
1492
- let defaultPrevented = false;
1493
- return {
1494
- preventDefault: () => {
1495
- defaultPrevented = true;
1496
- },
1497
- get defaultPrevented() {
1498
- return defaultPrevented;
1499
- }
1500
- };
1501
- }
1502
- function filterDropdownEntries(parsed, searchValue) {
1503
- const normalizedSearch = searchValue.trim().toLocaleLowerCase();
1504
- const matchedItems = new Set(parsed.items.filter((item) => item.label.toLocaleLowerCase().includes(normalizedSearch)).map((item) => item.id));
1505
- return {
1506
- ...parsed,
1507
- items: parsed.items.filter((item) => matchedItems.has(item.id)),
1508
- entries: parsed.entries.filter((entry) => entry.type !== "item" || matchedItems.has(entry.id))
1509
- };
1510
- }
1597
+ //#endregion
1598
+ //#region src/components/Dropdown/Dropdown.tsx
1511
1599
  const DropdownTriggerSlot = createDropdownSlot("trigger", "Dropdown.Trigger");
1512
1600
  const DropdownContentSlot = createDropdownSlot("content", "Dropdown.Content");
1513
1601
  const DropdownItemSlot = createDropdownSlot("item", "Dropdown.Item");
@@ -3199,9 +3287,6 @@ const createPresentationStyles = (theme) => StyleSheet.create({
3199
3287
  justifyContent: "center",
3200
3288
  padding: theme.tokens.spacing[4]
3201
3289
  },
3202
- popoverRoot: { padding: theme.tokens.spacing[4] },
3203
- popoverRootTop: { justifyContent: "flex-start" },
3204
- popoverRootBottom: { justifyContent: "flex-end" },
3205
3290
  backdrop: {
3206
3291
  ...StyleSheet.absoluteFill,
3207
3292
  backgroundColor: theme.semantic.overlay.backdrop
@@ -3228,17 +3313,8 @@ const createPresentationStyles = (theme) => StyleSheet.create({
3228
3313
  width: "100%",
3229
3314
  maxWidth: 420,
3230
3315
  maxHeight: "60%",
3231
- alignSelf: "center",
3232
3316
  borderRadius: theme.tokens.radius.lg
3233
3317
  },
3234
- popoverTop: {
3235
- marginBottom: theme.tokens.spacing[8],
3236
- alignSelf: "center"
3237
- },
3238
- popoverBottom: {
3239
- marginTop: theme.tokens.spacing[8],
3240
- alignSelf: "center"
3241
- },
3242
3318
  handleWrap: {
3243
3319
  alignItems: "center",
3244
3320
  paddingTop: theme.tokens.spacing[3]
@@ -3302,7 +3378,7 @@ const SelectModal = ({ visible, onClose, dismissOnBackdropPress, contentStyle, c
3302
3378
  SelectModal.displayName = "Select.Modal";
3303
3379
  //#endregion
3304
3380
  //#region src/components/Select/Presentation/SelectPopover.tsx
3305
- const SelectPopover = ({ visible, onClose, dismissOnBackdropPress, placement, matchTriggerWidth, triggerWidth, contentStyle, children }) => {
3381
+ const SelectPopover = ({ visible, onClose, dismissOnBackdropPress, position, onFloatingLayout, matchTriggerWidth, triggerWidth, contentStyle, children }) => {
3306
3382
  const styles = useThemeStyles(createPresentationStyles);
3307
3383
  return /* @__PURE__ */ jsx(Modal$1, {
3308
3384
  transparent: true,
@@ -3310,20 +3386,21 @@ const SelectPopover = ({ visible, onClose, dismissOnBackdropPress, placement, ma
3310
3386
  animationType: "fade",
3311
3387
  onRequestClose: onClose,
3312
3388
  children: /* @__PURE__ */ jsxs(View, {
3313
- style: [
3314
- styles.modalRoot,
3315
- styles.popoverRoot,
3316
- placement === "top" ? styles.popoverRootTop : styles.popoverRootBottom
3317
- ],
3389
+ style: styles.modalRoot,
3318
3390
  testID: "select-content-root",
3319
3391
  children: [/* @__PURE__ */ jsx(SelectBackdrop, {
3320
3392
  onClose,
3321
3393
  dismissOnBackdropPress
3322
3394
  }), /* @__PURE__ */ jsx(View, {
3395
+ onLayout: onFloatingLayout,
3323
3396
  style: [
3324
3397
  styles.content,
3325
3398
  styles.popover,
3326
- placement === "top" ? styles.popoverTop : styles.popoverBottom,
3399
+ {
3400
+ position: "absolute",
3401
+ top: position.top,
3402
+ left: position.left
3403
+ },
3327
3404
  matchTriggerWidth && triggerWidth ? { width: triggerWidth } : null,
3328
3405
  contentStyle
3329
3406
  ],
@@ -3582,7 +3659,7 @@ const SelectContentSurface = () => {
3582
3659
  const wasOpenRef = useRef(false);
3583
3660
  const [openCycle, setOpenCycle] = useState(0);
3584
3661
  const context = useSelectContext();
3585
- const { isOpen, resolvedPresentation, placement, dismissOnBackdropPress, contentStyle, matchTriggerWidth, triggerWidth, resolvedLabel, closeContent, searchable, loading, filteredRows, selectedValues, selectedOptions, maxSelected, optionStyle, selectOption, selectGroup, itemHeight, selectedRowIndex, query } = context;
3662
+ const { isOpen, resolvedPresentation, position, onFloatingLayout, dismissOnBackdropPress, contentStyle, matchTriggerWidth, triggerWidth, resolvedLabel, closeContent, searchable, loading, filteredRows, selectedValues, selectedOptions, maxSelected, optionStyle, selectOption, selectGroup, itemHeight, selectedRowIndex, query } = context;
3586
3663
  const initialScrollIndex = Boolean(context.virtual) && selectedRowIndex > 0 && query === "" ? selectedRowIndex : void 0;
3587
3664
  useEffect(() => {
3588
3665
  if (isOpen && !wasOpenRef.current) setOpenCycle((cycle) => cycle + 1);
@@ -3680,7 +3757,8 @@ const SelectContentSurface = () => {
3680
3757
  visible: isOpen,
3681
3758
  onClose: closeContent,
3682
3759
  dismissOnBackdropPress,
3683
- placement,
3760
+ position,
3761
+ onFloatingLayout,
3684
3762
  matchTriggerWidth,
3685
3763
  triggerWidth,
3686
3764
  contentStyle,
@@ -3730,13 +3808,6 @@ const useSelectCollection = (children, optionsProp) => {
3730
3808
  };
3731
3809
  };
3732
3810
  //#endregion
3733
- //#region src/components/Select/internal/useSelectPresentation.ts
3734
- const useSelectPresentation = (presentation = "auto") => {
3735
- const { width } = useWindowDimensions();
3736
- if (presentation === "auto") return width >= 768 ? "popover" : "sheet";
3737
- return presentation;
3738
- };
3739
- //#endregion
3740
3811
  //#region src/components/Select/internal/useSelectSearch.ts
3741
3812
  const useSelectSearch = ({ rows, isOpen, searchable, searchableFromChildren, onSearch, filterOptions, filter = defaultSelectFilter }) => {
3742
3813
  const [query, setQuery] = useState("");
@@ -4055,14 +4126,16 @@ const SelectValue = createSelectSlot("value", "Select.Value");
4055
4126
  //#endregion
4056
4127
  //#region src/components/Select/Root/SelectRoot.tsx
4057
4128
  function SelectRoot(props) {
4058
- const { label, description, error, invalid = false, required = false, disabled = false, placeholder = "Select...", color = "primary", variant = "outline", size, open, defaultOpen, onOpenChange, clearable = false, searchable: searchableProp, searchPlaceholder, loading = false, loadingText = "Loading...", onSearch, filterOptions, filter, empty, startIcon, endIcon, prefix, suffix, renderValue, renderOption, closeOnSelect, maxSelected, presentation = "auto", placement = "bottom", matchTriggerWidth = false, dismissOnBackdropPress = true, virtual, options: optionsProp, children, style, triggerStyle, textStyle, contentStyle, optionStyle, searchStyle, accessibilityLabel, accessibilityHint, testID } = props;
4129
+ const { label, description, error, invalid = false, required = false, disabled = false, placeholder = "Select...", color = "primary", variant = "outline", size, open, defaultOpen, onOpenChange, clearable = false, searchable: searchableProp, searchPlaceholder, loading = false, loadingText = "Loading...", onSearch, filterOptions, filter, empty, startIcon, endIcon, prefix, suffix, renderValue, renderOption, closeOnSelect, maxSelected, presentation = "auto", placement = "bottom-start", offset = 8, matchTriggerWidth = false, dismissOnBackdropPress = true, virtual, options: optionsProp, children, style, triggerStyle, textStyle, contentStyle, optionStyle, searchStyle, accessibilityLabel, accessibilityHint, testID } = props;
4059
4130
  const field = useFormFieldContext();
4060
4131
  const overlayId = useId();
4061
4132
  const hasOwnField = Boolean(label || description || error);
4062
4133
  const [triggerWidth, setTriggerWidth] = useState();
4134
+ const triggerRef = useRef(null);
4135
+ const { position, placement: resolvedPlacement, updatePosition, onFloatingLayout } = useNativeFloatingPosition(placement, offset);
4063
4136
  const searchInputRef = useRef(null);
4064
4137
  const selectedFocusValueRef = useRef(void 0);
4065
- const resolvedPresentation = useSelectPresentation(presentation);
4138
+ const resolvedPresentation = useOverlayPresentation(presentation);
4066
4139
  const { options, rows, searchableFromChildren, searchPlaceholderFromChildren, emptyFromChildren, loadingFromChildren } = useSelectCollection(children, optionsProp);
4067
4140
  const resolvedSize = size ?? field?.size ?? "md";
4068
4141
  const isInvalid = invalid || Boolean(error) || !hasOwnField && Boolean(field?.invalid);
@@ -4175,6 +4248,10 @@ function SelectRoot(props) {
4175
4248
  announce("Group selected");
4176
4249
  if (closeOnSelect) closeDropdown();
4177
4250
  };
4251
+ const openContent = () => {
4252
+ updatePosition(triggerRef);
4253
+ openDropdown();
4254
+ };
4178
4255
  const contextValue = {
4179
4256
  label,
4180
4257
  description,
@@ -4194,7 +4271,9 @@ function SelectRoot(props) {
4194
4271
  resolvedLabel,
4195
4272
  resolvedHint,
4196
4273
  resolvedPresentation,
4197
- placement,
4274
+ placement: resolvedPlacement,
4275
+ position,
4276
+ onFloatingLayout,
4198
4277
  dismissOnBackdropPress,
4199
4278
  matchTriggerWidth,
4200
4279
  triggerWidth,
@@ -4211,7 +4290,7 @@ function SelectRoot(props) {
4211
4290
  empty: empty ?? emptyFromChildren ?? "Nothing found",
4212
4291
  loadingContent: loadingFromChildren ?? loadingText,
4213
4292
  closeContent: dismiss.requestClose,
4214
- openContent: openDropdown,
4293
+ openContent,
4215
4294
  clearValue,
4216
4295
  selectOption,
4217
4296
  selectGroup,
@@ -4234,6 +4313,7 @@ function SelectRoot(props) {
4234
4313
  const control = /* @__PURE__ */ jsx(SelectContext.Provider, {
4235
4314
  value: contextValue,
4236
4315
  children: /* @__PURE__ */ jsxs(View, {
4316
+ ref: triggerRef,
4237
4317
  testID,
4238
4318
  onLayout: (event) => setTriggerWidth(event.nativeEvent.layout.width),
4239
4319
  children: [/* @__PURE__ */ jsx(SelectTrigger, {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vellira-ui/react-native",
3
- "version": "2.55.0",
3
+ "version": "2.57.0",
4
4
  "description": "React Native components for Vellira Design System",
5
5
  "keywords": [
6
6
  "vellira",