@vellira-ui/react-native 2.56.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 +123 -66
  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
304
+ };
305
+ };
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])
258
326
  };
259
327
  };
260
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);
@@ -1356,7 +1431,6 @@ function DropdownRoot({ children, label = "Menu", trigger, icon, arrowIcon, show
1356
1431
  const overlayId = useId();
1357
1432
  const [uncontrolledSearchValue, setUncontrolledSearchValue] = useState(defaultSearchValue);
1358
1433
  const resolvedSearchValue = searchValue ?? uncontrolledSearchValue;
1359
- const { width } = useWindowDimensions();
1360
1434
  const triggerRef = useRef(null);
1361
1435
  const setTriggerRef = useCallback((node) => {
1362
1436
  if (node && typeof node === "object" && "measureInWindow" in node && typeof node.measureInWindow === "function") {
@@ -1368,7 +1442,7 @@ function DropdownRoot({ children, label = "Menu", trigger, icon, arrowIcon, show
1368
1442
  const parsed = useMemo(() => parseDropdownChildren(children), [children]);
1369
1443
  const contentCommand = parsed.contentProps?.command ?? false;
1370
1444
  const isSearchable = searchable || command || contentCommand || !!parsed.searchProps;
1371
- const resolvedPresentation = presentation === "auto" ? width < 768 ? "sheet" : "popover" : presentation;
1445
+ const resolvedPresentation = useOverlayPresentation(presentation);
1372
1446
  const contentStyleFromSlot = parsed.contentProps?.style;
1373
1447
  const contentPresentation = (parsed.contentProps?.presentation === "auto" ? void 0 : parsed.contentProps?.presentation) ?? resolvedPresentation;
1374
1448
  const { position, updatePosition, onFloatingLayout } = useNativeFloatingPosition(placement, offset);
@@ -1410,20 +1484,11 @@ function DropdownRoot({ children, label = "Menu", trigger, icon, arrowIcon, show
1410
1484
  if (!isOpen) return;
1411
1485
  AccessibilityInfo.announceForAccessibility(`${menuAccessibilityLabel} opened`);
1412
1486
  }, [isOpen, menuAccessibilityLabel]);
1413
- const focusTrigger = useCallback(() => {
1414
- if (Platform.OS === "web") {
1415
- const triggerNode = triggerRef.current;
1416
- if (triggerNode && typeof triggerNode === "object" && "focus" in triggerNode && typeof triggerNode.focus === "function") triggerNode.focus();
1417
- return;
1418
- }
1419
- if (typeof findNodeHandle !== "function") return;
1420
- const handle = findNodeHandle(triggerRef.current);
1421
- if (handle && AccessibilityInfo.setAccessibilityFocus) AccessibilityInfo.setAccessibilityFocus(handle);
1422
- }, []);
1487
+ const { restoreFocusAfterClose } = useOverlayFocusRestore({ triggerRef });
1423
1488
  const closeAndFocusTrigger = useCallback(() => {
1424
1489
  closeDropdown();
1425
- requestAnimationFrame(focusTrigger);
1426
- }, [closeDropdown, focusTrigger]);
1490
+ restoreFocusAfterClose();
1491
+ }, [closeDropdown, restoreFocusAfterClose]);
1427
1492
  const dismiss = useOverlayDismiss({
1428
1493
  id: overlayId,
1429
1494
  active: isOpen,
@@ -3222,9 +3287,6 @@ const createPresentationStyles = (theme) => StyleSheet.create({
3222
3287
  justifyContent: "center",
3223
3288
  padding: theme.tokens.spacing[4]
3224
3289
  },
3225
- popoverRoot: { padding: theme.tokens.spacing[4] },
3226
- popoverRootTop: { justifyContent: "flex-start" },
3227
- popoverRootBottom: { justifyContent: "flex-end" },
3228
3290
  backdrop: {
3229
3291
  ...StyleSheet.absoluteFill,
3230
3292
  backgroundColor: theme.semantic.overlay.backdrop
@@ -3251,17 +3313,8 @@ const createPresentationStyles = (theme) => StyleSheet.create({
3251
3313
  width: "100%",
3252
3314
  maxWidth: 420,
3253
3315
  maxHeight: "60%",
3254
- alignSelf: "center",
3255
3316
  borderRadius: theme.tokens.radius.lg
3256
3317
  },
3257
- popoverTop: {
3258
- marginBottom: theme.tokens.spacing[8],
3259
- alignSelf: "center"
3260
- },
3261
- popoverBottom: {
3262
- marginTop: theme.tokens.spacing[8],
3263
- alignSelf: "center"
3264
- },
3265
3318
  handleWrap: {
3266
3319
  alignItems: "center",
3267
3320
  paddingTop: theme.tokens.spacing[3]
@@ -3325,7 +3378,7 @@ const SelectModal = ({ visible, onClose, dismissOnBackdropPress, contentStyle, c
3325
3378
  SelectModal.displayName = "Select.Modal";
3326
3379
  //#endregion
3327
3380
  //#region src/components/Select/Presentation/SelectPopover.tsx
3328
- const SelectPopover = ({ visible, onClose, dismissOnBackdropPress, placement, matchTriggerWidth, triggerWidth, contentStyle, children }) => {
3381
+ const SelectPopover = ({ visible, onClose, dismissOnBackdropPress, position, onFloatingLayout, matchTriggerWidth, triggerWidth, contentStyle, children }) => {
3329
3382
  const styles = useThemeStyles(createPresentationStyles);
3330
3383
  return /* @__PURE__ */ jsx(Modal$1, {
3331
3384
  transparent: true,
@@ -3333,20 +3386,21 @@ const SelectPopover = ({ visible, onClose, dismissOnBackdropPress, placement, ma
3333
3386
  animationType: "fade",
3334
3387
  onRequestClose: onClose,
3335
3388
  children: /* @__PURE__ */ jsxs(View, {
3336
- style: [
3337
- styles.modalRoot,
3338
- styles.popoverRoot,
3339
- placement === "top" ? styles.popoverRootTop : styles.popoverRootBottom
3340
- ],
3389
+ style: styles.modalRoot,
3341
3390
  testID: "select-content-root",
3342
3391
  children: [/* @__PURE__ */ jsx(SelectBackdrop, {
3343
3392
  onClose,
3344
3393
  dismissOnBackdropPress
3345
3394
  }), /* @__PURE__ */ jsx(View, {
3395
+ onLayout: onFloatingLayout,
3346
3396
  style: [
3347
3397
  styles.content,
3348
3398
  styles.popover,
3349
- placement === "top" ? styles.popoverTop : styles.popoverBottom,
3399
+ {
3400
+ position: "absolute",
3401
+ top: position.top,
3402
+ left: position.left
3403
+ },
3350
3404
  matchTriggerWidth && triggerWidth ? { width: triggerWidth } : null,
3351
3405
  contentStyle
3352
3406
  ],
@@ -3605,7 +3659,7 @@ const SelectContentSurface = () => {
3605
3659
  const wasOpenRef = useRef(false);
3606
3660
  const [openCycle, setOpenCycle] = useState(0);
3607
3661
  const context = useSelectContext();
3608
- 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;
3609
3663
  const initialScrollIndex = Boolean(context.virtual) && selectedRowIndex > 0 && query === "" ? selectedRowIndex : void 0;
3610
3664
  useEffect(() => {
3611
3665
  if (isOpen && !wasOpenRef.current) setOpenCycle((cycle) => cycle + 1);
@@ -3703,7 +3757,8 @@ const SelectContentSurface = () => {
3703
3757
  visible: isOpen,
3704
3758
  onClose: closeContent,
3705
3759
  dismissOnBackdropPress,
3706
- placement,
3760
+ position,
3761
+ onFloatingLayout,
3707
3762
  matchTriggerWidth,
3708
3763
  triggerWidth,
3709
3764
  contentStyle,
@@ -3753,13 +3808,6 @@ const useSelectCollection = (children, optionsProp) => {
3753
3808
  };
3754
3809
  };
3755
3810
  //#endregion
3756
- //#region src/components/Select/internal/useSelectPresentation.ts
3757
- const useSelectPresentation = (presentation = "auto") => {
3758
- const { width } = useWindowDimensions();
3759
- if (presentation === "auto") return width >= 768 ? "popover" : "sheet";
3760
- return presentation;
3761
- };
3762
- //#endregion
3763
3811
  //#region src/components/Select/internal/useSelectSearch.ts
3764
3812
  const useSelectSearch = ({ rows, isOpen, searchable, searchableFromChildren, onSearch, filterOptions, filter = defaultSelectFilter }) => {
3765
3813
  const [query, setQuery] = useState("");
@@ -4078,14 +4126,16 @@ const SelectValue = createSelectSlot("value", "Select.Value");
4078
4126
  //#endregion
4079
4127
  //#region src/components/Select/Root/SelectRoot.tsx
4080
4128
  function SelectRoot(props) {
4081
- 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;
4082
4130
  const field = useFormFieldContext();
4083
4131
  const overlayId = useId();
4084
4132
  const hasOwnField = Boolean(label || description || error);
4085
4133
  const [triggerWidth, setTriggerWidth] = useState();
4134
+ const triggerRef = useRef(null);
4135
+ const { position, placement: resolvedPlacement, updatePosition, onFloatingLayout } = useNativeFloatingPosition(placement, offset);
4086
4136
  const searchInputRef = useRef(null);
4087
4137
  const selectedFocusValueRef = useRef(void 0);
4088
- const resolvedPresentation = useSelectPresentation(presentation);
4138
+ const resolvedPresentation = useOverlayPresentation(presentation);
4089
4139
  const { options, rows, searchableFromChildren, searchPlaceholderFromChildren, emptyFromChildren, loadingFromChildren } = useSelectCollection(children, optionsProp);
4090
4140
  const resolvedSize = size ?? field?.size ?? "md";
4091
4141
  const isInvalid = invalid || Boolean(error) || !hasOwnField && Boolean(field?.invalid);
@@ -4198,6 +4248,10 @@ function SelectRoot(props) {
4198
4248
  announce("Group selected");
4199
4249
  if (closeOnSelect) closeDropdown();
4200
4250
  };
4251
+ const openContent = () => {
4252
+ updatePosition(triggerRef);
4253
+ openDropdown();
4254
+ };
4201
4255
  const contextValue = {
4202
4256
  label,
4203
4257
  description,
@@ -4217,7 +4271,9 @@ function SelectRoot(props) {
4217
4271
  resolvedLabel,
4218
4272
  resolvedHint,
4219
4273
  resolvedPresentation,
4220
- placement,
4274
+ placement: resolvedPlacement,
4275
+ position,
4276
+ onFloatingLayout,
4221
4277
  dismissOnBackdropPress,
4222
4278
  matchTriggerWidth,
4223
4279
  triggerWidth,
@@ -4234,7 +4290,7 @@ function SelectRoot(props) {
4234
4290
  empty: empty ?? emptyFromChildren ?? "Nothing found",
4235
4291
  loadingContent: loadingFromChildren ?? loadingText,
4236
4292
  closeContent: dismiss.requestClose,
4237
- openContent: openDropdown,
4293
+ openContent,
4238
4294
  clearValue,
4239
4295
  selectOption,
4240
4296
  selectGroup,
@@ -4257,6 +4313,7 @@ function SelectRoot(props) {
4257
4313
  const control = /* @__PURE__ */ jsx(SelectContext.Provider, {
4258
4314
  value: contextValue,
4259
4315
  children: /* @__PURE__ */ jsxs(View, {
4316
+ ref: triggerRef,
4260
4317
  testID,
4261
4318
  onLayout: (event) => setTriggerWidth(event.nativeEvent.layout.width),
4262
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.56.0",
3
+ "version": "2.57.0",
4
4
  "description": "React Native components for Vellira Design System",
5
5
  "keywords": [
6
6
  "vellira",