diff --git a/example/app/package.json b/example/app/package.json index a48096da..93aba0f6 100644 --- a/example/app/package.json +++ b/example/app/package.json @@ -5,20 +5,25 @@ "@fortawesome/free-solid-svg-icons": "6.5.2", "@fortawesome/react-native-fontawesome": "0.3.2", "@react-native-async-storage/async-storage": "2.2.0", + "@react-native-menu/menu": "2.0.0", "@react-navigation/bottom-tabs": "7.3.10", "@react-navigation/native": "7.0.14", "@react-navigation/native-stack": "7.2.0", "@shopify/flash-list": "2.3.1", "react": "19.2.3", "react-native": "0.86.0", + "react-native-context-menu-view": "1.21.0", "react-native-gesture-handler": "2.32.0", "react-native-haptic-feedback": "2.3.3", + "react-native-ios-context-menu": "3.1.0", + "react-native-ios-utilities": "5.1.2", "react-native-reanimated": "4.5.1", "react-native-safe-area-context": "5.7.0", "react-native-screens": "4.25.2", "react-native-sortables": "workspace:*", "react-native-svg": "15.15.4", - "react-native-worklets": "0.10.1" + "react-native-worklets": "0.10.1", + "zeego": "3.0.6" }, "devDependencies": { "@babel/core": "^7.25.2", diff --git a/example/app/src/examples/SortableGrid/tests/ContextMenuExample.tsx b/example/app/src/examples/SortableGrid/tests/ContextMenuExample.tsx new file mode 100644 index 00000000..216498a1 --- /dev/null +++ b/example/app/src/examples/SortableGrid/tests/ContextMenuExample.tsx @@ -0,0 +1,110 @@ +import { MenuView } from '@react-native-menu/menu'; +import { useCallback, useState } from 'react'; +import { runOnJS, useAnimatedReaction } from 'react-native-reanimated'; +import type { SortableGridRenderItem } from 'react-native-sortables'; +import Sortable, { useItemContext } from 'react-native-sortables'; + +import { GridCard, Section } from '@/components'; +import { spacing } from '@/theme'; +import { getItems } from '@/utils'; + +const DATA = getItems(6); +const COLUMNS = 2; + +// The native menu opens on the OS long-press timeout (~500ms, not tunable), so +// tune the sortable side: pick up sooner and treat a small move as a drag. +const DRAG_ACTIVATION_DELAY = 120; // ms until draggable (default 200) +// Small enough that the sortable claims the touch before the surrounding +// ScrollView starts scrolling, but larger than the finger jitter of a +// stationary long press (which must be left to the native context menu). +const DRAG_FAIL_OFFSET = 8; // px of movement that counts as a drag (default 5) + +const MENU_ACTIONS = [ + { id: 'edit', title: 'Edit' }, + { attributes: { destructive: true }, id: 'delete', title: 'Delete' } +]; + +function noop() { + // Menu actions are no-ops in this example. +} + +// Baseline for comparison: the native context menu on a PLAIN view (no +// sortable). Long press it to see how the OS lifts the view and keeps it +// scaled up while the menu is open. +function PlainMenuCard() { + return ( + // Without an explicit size the native MenuView stretches to fill the row, + // so the long press triggers anywhere in the container, not just on the + // card. Size it to the card so the hit area matches what you see. + + + Plain + + + ); +} + +function MenuCard({ item }: { item: string }) { + const { isDragging } = useItemContext(); + const [dragging, setDragging] = useState(false); + + useAnimatedReaction( + () => isDragging.value, + (value, prev) => { + if (value !== prev) { + runOnJS(setDragging)(value); + } + } + ); + + // While dragging, drop the menu so it can't open (and closes if already open). + // A stationary long press keeps it. + if (dragging) { + return {item}; + } + + return ( + + {item} + + ); +} + +export default function ContextMenuExample() { + const renderItem = useCallback>( + ({ item }) => , + [] + ); + + return ( + <> +
+ +
+
+ +
+ + ); +} diff --git a/example/app/src/examples/SortableGrid/tests/ContextMenuViewExample.tsx b/example/app/src/examples/SortableGrid/tests/ContextMenuViewExample.tsx new file mode 100644 index 00000000..a213e40c --- /dev/null +++ b/example/app/src/examples/SortableGrid/tests/ContextMenuViewExample.tsx @@ -0,0 +1,124 @@ +// Grid cards with a native iOS context menu (react-native-context-menu-view). +// Long press a card and the OS lifts it and opens the menu (it stays open); a +// drag reorders. Active drag-reorder and the native lift fundamentally fight - +// the drag arms on the long press and the OS cancels that touch when the menu +// commits - so the card shows a small scale-down settle as the menu opens. +import { useCallback, useState } from 'react'; +import { View } from 'react-native'; +import ContextMenu from 'react-native-context-menu-view'; +import { runOnJS, useAnimatedReaction } from 'react-native-reanimated'; +import type { SortableGridRenderItem } from 'react-native-sortables'; +import Sortable, { useItemContext } from 'react-native-sortables'; + +import { GridCard, Section } from '@/components'; +import { radius, spacing } from '@/theme'; +import { getItems } from '@/utils'; + +const DATA = getItems(6); +const COLUMNS = 2; + +// The native menu opens on the OS long-press timeout (~500ms, not tunable), so +// tune the sortable side: pick up sooner and treat a small move as a drag. +const DRAG_ACTIVATION_DELAY = 120; // ms until draggable (default 200) +const DRAG_FAIL_OFFSET = 8; // px of movement that counts as a drag (default 5) + +const MENU_ACTIONS = [ + { systemIcon: 'pencil', title: 'Edit' }, + { destructive: true, systemIcon: 'trash', title: 'Delete' } +]; + +function noop() { + // Menu actions are no-ops in this example. +} + +// Baseline (no sortable): the native menu lifts the card and keeps it scaled up +// while the menu is open. +function PlainMenuCard() { + return ( + + + Plain + + + ); +} + +function MenuCard({ item }: { item: string }) { + const { isDragging } = useItemContext(); + const [dragging, setDragging] = useState(false); + // Grid cards stretch to fill their cell (no explicit size). When the OS lifts + // such a card for the preview it has nothing to size against and collapses to + // nothing (the card "disappears"), so render an explicitly sized `preview`. + // Measure the cell on a plain wrapper View - onLayout fires reliably there - + // and mirror that size into the preview card. + const [size, setSize] = useState({ height: 170, width: 170 }); + + useAnimatedReaction( + () => isDragging.value, + (value, prev) => { + if (value !== prev) { + runOnJS(setDragging)(value); + } + } + ); + + return ( + // Keep the ContextMenu mounted and just disable it while dragging - a + // stationary long press keeps the menu; a drag drops it (and reorders). + + {item} + + } + disableShadow + onPress={noop}> + + setSize({ height: layout.height, width: layout.width }) + }> + {item} + + + ); +} + +export default function ContextMenuViewExample() { + const renderItem = useCallback>( + ({ item }) => , + [] + ); + + return ( + <> +
+ +
+
+ +
+ + ); +} diff --git a/example/app/src/examples/SortableGrid/tests/ContextMenusExample.tsx b/example/app/src/examples/SortableGrid/tests/ContextMenusExample.tsx new file mode 100644 index 00000000..ec289526 --- /dev/null +++ b/example/app/src/examples/SortableGrid/tests/ContextMenusExample.tsx @@ -0,0 +1,83 @@ +import { useState } from 'react'; +import { Pressable, StyleSheet, Text, View } from 'react-native'; +import Sortable from 'react-native-sortables'; + +import { ScrollScreen } from '@/components'; +import { colors, radius, spacing } from '@/theme'; + +import ContextMenuExample from './ContextMenuExample'; +import ContextMenuViewExample from './ContextMenuViewExample'; + +// Two ways to attach a native iOS context menu to sortable grid items, one per +// tab. They behave the same on a long press; the difference is the library. +const TABS = [ + { + Content: ContextMenuExample, + key: 'menu', + label: '@react-native-menu/menu' + }, + { + Content: ContextMenuViewExample, + key: 'view', + label: 'react-native-context-menu-view' + } +]; + +export default function ContextMenusExample() { + const [activeKey, setActiveKey] = useState(TABS[0]!.key); + const ActiveContent = (TABS.find(tab => tab.key === activeKey) ?? TABS[0]!) + .Content; + + return ( + + + {TABS.map(({ key, label }) => { + const active = key === activeKey; + return ( + setActiveKey(key)}> + + {label} + + + ); + })} + + {/* SortableLayer lets the dragged item render above the sections. */} + + + + + ); +} + +const styles = StyleSheet.create({ + tab: { + alignItems: 'center', + backgroundColor: colors.background3, + borderRadius: radius.sm, + flex: 1, + paddingHorizontal: spacing.xs, + paddingVertical: spacing.sm + }, + tabActive: { + backgroundColor: colors.primary + }, + tabBar: { + flexDirection: 'row', + gap: spacing.xs, + marginBottom: spacing.md + }, + tabLabel: { + color: colors.foreground2, + fontSize: 12, + fontWeight: '600' + }, + tabLabelActive: { + color: colors.background1 + } +}); diff --git a/example/app/src/examples/SortableGrid/tests/index.ts b/example/app/src/examples/SortableGrid/tests/index.ts index 4451576a..5109516a 100644 --- a/example/app/src/examples/SortableGrid/tests/index.ts +++ b/example/app/src/examples/SortableGrid/tests/index.ts @@ -1,4 +1,5 @@ export { default as BottomTabsNavigatorExample } from './BottomTabsNavigatorExample'; +export { default as ContextMenusExample } from './ContextMenusExample'; export { default as HidingItemsExample } from './HidingItemsExample'; export { default as InputFocusExample } from './InputFocusExample'; export { default as MaxOverscrollOffsetExample } from './MaxOverscrollOffsetExample'; diff --git a/example/app/src/examples/navigation/routes.ts b/example/app/src/examples/navigation/routes.ts index ee52357d..01380aa0 100644 --- a/example/app/src/examples/navigation/routes.ts +++ b/example/app/src/examples/navigation/routes.ts @@ -92,6 +92,12 @@ const routes: Routes = { Component: SortableGrid.tests.BottomTabsNavigatorExample, name: 'Bottom Tabs Navigator' }, + ...(IS_IOS && { + ContextMenu: { + Component: SortableGrid.tests.ContextMenusExample, + name: 'Context Menu' + } + }), HidingItems: { Component: SortableGrid.tests.HidingItemsExample, name: 'Hiding Items' diff --git a/packages/react-native-sortables/src/providers/shared/CommonValuesProvider.ts b/packages/react-native-sortables/src/providers/shared/CommonValuesProvider.ts index 35e26e9d..e1327894 100644 --- a/packages/react-native-sortables/src/providers/shared/CommonValuesProvider.ts +++ b/packages/react-native-sortables/src/providers/shared/CommonValuesProvider.ts @@ -100,6 +100,7 @@ const { CommonValuesContext, CommonValuesProvider, useCommonValuesContext } = const activeAnimationProgress = useMutableValue(0); const inactiveAnimationProgress = useMutableValue(0); const activeItemDropped = useMutableValue(true); + const activeItemBroughtToFront = useMutableValue(false); // ITEM ACTIVATION SETTINGS const dragActivationDelay = useAnimatableValue(_dragActivationDelay); @@ -148,6 +149,7 @@ const { CommonValuesContext, CommonValuesProvider, useCommonValuesContext } = activationAnimationDuration, activationState, activeAnimationProgress, + activeItemBroughtToFront, activeItemDimensions, activeItemDropped, activeItemKey, diff --git a/packages/react-native-sortables/src/providers/shared/DragProvider.ts b/packages/react-native-sortables/src/providers/shared/DragProvider.ts index a8c1702e..e278fbee 100644 --- a/packages/react-native-sortables/src/providers/shared/DragProvider.ts +++ b/packages/react-native-sortables/src/providers/shared/DragProvider.ts @@ -81,6 +81,7 @@ const { DragProvider, useDragContext } = createProvider('Drag')< activationAnimationDuration, activationState, activeAnimationProgress, + activeItemBroughtToFront, activeItemDimensions, activeItemDropped, activeItemKey, @@ -289,6 +290,7 @@ const { DragProvider, useDragContext } = createProvider('Drag')< activeItemPosition.value = itemPosition; activeItemDimensions.value = itemDimensions; activationState.value = DragActivationState.ACTIVE; + activeItemBroughtToFront.value = false; ctx.dragStartIndex = keyToIndex.value[key] ?? -1; if (activeContainerId) { @@ -298,7 +300,7 @@ const { DragProvider, useDragContext } = createProvider('Drag')< multiZoneActiveItemDimensions.value = itemDimensions; } - updateLayer?.(LayerState.FOCUSED); + // zIndex is elevated on the first drag move (see handleTouchesMove) // We need to update the custom handle measurements if the custom handle // is used (touch position is relative to the handle in this case) @@ -349,6 +351,7 @@ const { DragProvider, useDragContext } = createProvider('Drag')< activeAnimationProgress, activeContainerId, activeHandleOffset, + activeItemBroughtToFront, activeItemDimensions, activeItemDropped, activationState, @@ -366,7 +369,6 @@ const { DragProvider, useDragContext } = createProvider('Drag')< prevActiveItemKey, onDragStart, touchPosition, - updateLayer, updateActiveHandleMeasurements ] ); @@ -457,6 +459,14 @@ const { DragProvider, useDragContext } = createProvider('Drag')< } const ctx = context.value; + const startTouch = ctx.touchStartTouch; + + let movedDistance = 0; + if (startTouch) { + const dX = touch.absoluteX - startTouch.absoluteX; + const dY = touch.absoluteY - startTouch.absoluteY; + movedDistance = Math.sqrt(dX * dX + dY * dY); + } if (activeItemKey.value) { onDragMove({ @@ -464,25 +474,19 @@ const { DragProvider, useDragContext } = createProvider('Drag')< key: activeItemKey.value, touchData: touch }); - } - if (activationState.value === DragActivationState.TOUCHED) { - if (!ctx.touchStartTouch) { - fail(); - return; - } - const dX = touch.absoluteX - ctx.touchStartTouch.absoluteX; - const dY = touch.absoluteY - ctx.touchStartTouch.absoluteY; - - // Cancel touch if the touch moved too far from the initial position - // before the item activation animation starts - const r = Math.sqrt(dX * dX + dY * dY); + // Elevate only once dragged, so a bare long press doesn't change the + // stacking context and dismiss a native context menu (#417) if ( - // activeItemKey is set after the drag activation delay passes - // and we don't want to cancel the touch anymore after this time - activeItemKey.value === null && - r >= dragActivationFailOffset.value + !activeItemBroughtToFront.value && + movedDistance >= dragActivationFailOffset.value ) { + activeItemBroughtToFront.value = true; + updateLayer?.(LayerState.FOCUSED); + } + } else if (activationState.value === DragActivationState.TOUCHED) { + // Cancel the touch if it moves before activation (it is a scroll) + if (!startTouch || movedDistance >= dragActivationFailOffset.value) { fail(); return; } @@ -490,11 +494,13 @@ const { DragProvider, useDragContext } = createProvider('Drag')< }, [ activationState, + activeItemBroughtToFront, activeItemKey, dragActivationFailOffset, currentTouch, context, - onDragMove + onDragMove, + updateLayer ] ); @@ -549,7 +555,12 @@ const { DragProvider, useDragContext } = createProvider('Drag')< inactiveAnimationProgress.value = animate(); activeAnimationProgress.value = animate(); - updateLayer?.(LayerState.INTERMEDIATE); + // Restore the layer only if it was elevated. The flag resets on the next + // drag start, not here, to avoid racing an overlapping drag. + const wasBroughtToFront = activeItemBroughtToFront.value; + if (wasBroughtToFront) { + updateLayer?.(LayerState.INTERMEDIATE); + } haptics.medium(); stableOnDragEnd({ @@ -562,7 +573,9 @@ const { DragProvider, useDragContext } = createProvider('Drag')< setAnimatedTimeout(() => { activeItemDropped.value = true; - updateLayer?.(LayerState.IDLE); + if (wasBroughtToFront) { + updateLayer?.(LayerState.IDLE); + } onActiveItemDropped({ fromIndex, indexToKey: indexToKey.value, @@ -574,6 +587,7 @@ const { DragProvider, useDragContext } = createProvider('Drag')< }, [ activeContainerId, + activeItemBroughtToFront, activeItemKey, activeItemDimensions, activeItemDropped, diff --git a/packages/react-native-sortables/src/providers/shared/ItemContextProvider.ts b/packages/react-native-sortables/src/providers/shared/ItemContextProvider.ts index 732164bf..d50e4d80 100644 --- a/packages/react-native-sortables/src/providers/shared/ItemContextProvider.ts +++ b/packages/react-native-sortables/src/providers/shared/ItemContextProvider.ts @@ -1,4 +1,5 @@ import type { PropsWithChildren } from 'react'; +import { useDerivedValue } from 'react-native-reanimated'; import type { ItemContextType } from '../../types'; import { createProvider } from '../utils'; @@ -20,12 +21,17 @@ const { ItemContextProvider, useItemContextContext: useItemContext } = >(({ activationAnimationProgress, gesture, isActive, itemKey }) => { const { activationState, + activeItemBroughtToFront, activeItemKey, indexToKey, keyToIndex, prevActiveItemKey } = useCommonValuesContext(); + const isDragging = useDerivedValue( + () => isActive.value && activeItemBroughtToFront.value + ); + return { value: { activationAnimationProgress, @@ -34,6 +40,7 @@ const { ItemContextProvider, useItemContextContext: useItemContext } = gesture, indexToKey, isActive, + isDragging, itemKey, keyToIndex, prevActiveItemKey diff --git a/packages/react-native-sortables/src/providers/shared/hooks/useItemDecoration.ts b/packages/react-native-sortables/src/providers/shared/hooks/useItemDecoration.ts index e386a262..ebe3e9e1 100644 --- a/packages/react-native-sortables/src/providers/shared/hooks/useItemDecoration.ts +++ b/packages/react-native-sortables/src/providers/shared/hooks/useItemDecoration.ts @@ -17,6 +17,7 @@ export default function useItemDecoration( ): SharedValue { const { activationAnimationDuration, + activeItemBroughtToFront, activeItemOpacity, activeItemScale, activeItemShadowOpacity, @@ -38,8 +39,23 @@ export default function useItemDecoration( ); }); + // Ramp the "picked up" progress in when the item is actually brought to front + // (the first drag move), so the scale/shadow animate up smoothly even if the + // activation timing already finished during a long hold. Gating the raw + // progress alone would make it jump 0 -> 1 in a single frame in that case. + const broughtToFrontProgress = useDerivedValue(() => + activeItemBroughtToFront.value + ? withTiming(1, { duration: activationAnimationDuration.value }) + : 0 + ); + return useDerivedValue(() => { - const progress = activationAnimationProgress.value; + // Apply the "picked up" scale/shadow only once the item is actually dragged, + // not on a bare long press (otherwise a native context menu taking over the + // gesture makes the item scale up and then back down). The drop animation + // still runs via activationAnimationProgress falling back to 0. + const progress = + broughtToFrontProgress.value * activationAnimationProgress.value; const zeroProgressOpacity = interpolate( adjustedInactiveProgress.value, [0, 1], diff --git a/packages/react-native-sortables/src/providers/shared/hooks/useItemLayout.ts b/packages/react-native-sortables/src/providers/shared/hooks/useItemLayout.ts index 8f1cf278..c9a46c79 100644 --- a/packages/react-native-sortables/src/providers/shared/hooks/useItemLayout.ts +++ b/packages/react-native-sortables/src/providers/shared/hooks/useItemLayout.ts @@ -88,14 +88,30 @@ function useItemLayoutFabric( position: SharedValue, zIndex: SharedValue ): SharedValue { - const { activeItemDropped, usesAbsoluteLayout } = useCommonValuesContext(); + const { activeItemBroughtToFront, activeItemDropped, usesAbsoluteLayout } = + useCommonValuesContext(); const transformStartPosition = useMutableValue(null); const debounce = useAnimatedDebounce(); useAnimatedReaction( - () => ({ current: position.value, dropped: activeItemDropped.value }), - ({ current, dropped }) => { - transformStartPosition.value ??= current; + () => ({ + broughtToFront: activeItemBroughtToFront.value, + current: position.value, + dropped: activeItemDropped.value + }), + ({ broughtToFront, current, dropped }) => { + // Only switch to transform-based positioning once the item is actually + // dragged (brought to front). A bare long press that never turns into a + // drag then stays on a stable left/top-only style and never toggles the + // transform<->layout shape, so it never re-commits a frame to the item + // subtree. That matters on Fabric because a frame/onLayout on the item + // while a native context menu is lifting it (the menu portals the live + // view) invalidates the lift and snaps the item back into place. For a + // stationary item the transform deltas are always 0, so skipping the + // transform branch is visually identical. + if (broughtToFront) { + transformStartPosition.value ??= current; + } if (dropped) { debounce.schedule(() => { transformStartPosition.value = null; diff --git a/packages/react-native-sortables/src/providers/shared/hooks/useItemZIndex.test.ts b/packages/react-native-sortables/src/providers/shared/hooks/useItemZIndex.test.ts new file mode 100644 index 00000000..b92ab706 --- /dev/null +++ b/packages/react-native-sortables/src/providers/shared/hooks/useItemZIndex.test.ts @@ -0,0 +1,130 @@ +import { renderHook } from '@testing-library/react-hooks'; +import type { SharedValue } from 'react-native-reanimated'; +import { makeMutable } from 'react-native-reanimated'; + +import { useCommonValuesContext } from '../CommonValuesProvider'; +import useItemZIndex from './useItemZIndex'; + +jest.mock('../CommonValuesProvider', () => ({ + useCommonValuesContext: jest.fn() +})); + +const mockedUseCommonValuesContext = useCommonValuesContext as jest.Mock; + +type ContextOverrides = { + activeItemBroughtToFront?: boolean; + activeItemKey?: null | string; + prevActiveItemKey?: null | string; + isStackingOrderDesc?: boolean; + indexToKey?: Array; +}; + +function setupContext({ + activeItemBroughtToFront = false, + activeItemKey = null, + indexToKey = ['a', 'b', 'c'], + isStackingOrderDesc = false, + prevActiveItemKey = null +}: ContextOverrides) { + const keyToIndex = Object.fromEntries(indexToKey.map((key, i) => [key, i])); + mockedUseCommonValuesContext.mockReturnValue({ + activeItemBroughtToFront: makeMutable(activeItemBroughtToFront), + activeItemKey: makeMutable(activeItemKey), + indexToKey: makeMutable(indexToKey), + isStackingOrderDesc, + keyToIndex: makeMutable(keyToIndex), + prevActiveItemKey: makeMutable(prevActiveItemKey) + }); +} + +function renderZIndex(key: string, activationProgress: number): number { + const progress = makeMutable(activationProgress); + const { result } = renderHook(() => useItemZIndex(key, progress)) as { + result: { current: SharedValue }; + }; + return result.current.value; +} + +// itemCount = 3 in all cases below + +describe(useItemZIndex, () => { + describe('when the active item has NOT been brought to front (long press)', () => { + it('keeps the active item at its plain order zIndex', () => { + setupContext({ + activeItemBroughtToFront: false, + activeItemKey: 'a' + }); + // active + activation running, but not brought to front => order zIndex + expect(renderZIndex('a', 1)).toBe(0); + }); + + it('keeps inactive items at their plain order zIndex', () => { + setupContext({ + activeItemBroughtToFront: false, + activeItemKey: 'a' + }); + expect(renderZIndex('b', 1)).toBe(1); + expect(renderZIndex('c', 1)).toBe(2); + }); + + it('respects descending stacking order', () => { + setupContext({ + activeItemBroughtToFront: false, + activeItemKey: 'a', + isStackingOrderDesc: true + }); + // desc order zIndex for index 0 of 3 items => 3 - 0 - 1 = 2 + expect(renderZIndex('a', 1)).toBe(2); + }); + }); + + describe('when the active item HAS been brought to front (real drag)', () => { + it('elevates the active item above everything', () => { + setupContext({ + activeItemBroughtToFront: true, + activeItemKey: 'a' + }); + // 2 * itemCount + 1 = 7 + expect(renderZIndex('a', 1)).toBe(7); + }); + + it('lifts inactive items by itemCount while the activation runs', () => { + setupContext({ + activeItemBroughtToFront: true, + activeItemKey: 'a' + }); + // itemCount + orderZIndex + expect(renderZIndex('b', 1)).toBe(4); + expect(renderZIndex('c', 1)).toBe(5); + }); + + it('keeps the previously active item above siblings during the drop', () => { + setupContext({ + activeItemBroughtToFront: true, + activeItemKey: null, + prevActiveItemKey: 'a' + }); + // 2 * itemCount = 6 for the dropping item + expect(renderZIndex('a', 0.5)).toBe(6); + // other items are lifted by itemCount while the drop animates + expect(renderZIndex('b', 0.5)).toBe(4); + }); + }); + + describe('at rest (no active item, animation finished)', () => { + it.each([false, true])( + 'returns the plain order zIndex (broughtToFront=%s)', + broughtToFront => { + setupContext({ + activeItemBroughtToFront: broughtToFront, + activeItemKey: null, + prevActiveItemKey: 'a' + }); + // a stale broughtToFront=true is harmless at rest + expect(renderZIndex('a', 0)).toBe(0); + expect(renderZIndex('b', 0)).toBe(1); + expect(renderZIndex('c', 0)).toBe(2); + } + ); + }); +}); diff --git a/packages/react-native-sortables/src/providers/shared/hooks/useItemZIndex.ts b/packages/react-native-sortables/src/providers/shared/hooks/useItemZIndex.ts index fc411a13..985a3567 100644 --- a/packages/react-native-sortables/src/providers/shared/hooks/useItemZIndex.ts +++ b/packages/react-native-sortables/src/providers/shared/hooks/useItemZIndex.ts @@ -8,6 +8,7 @@ export default function useItemZIndex( activationAnimationProgress: SharedValue ): SharedValue { const { + activeItemBroughtToFront, activeItemKey, indexToKey, isStackingOrderDesc, @@ -17,16 +18,20 @@ export default function useItemZIndex( return useDerivedValue(() => { const itemCount = indexToKey.value.length; - - if (activeItemKey.value === key) { - return 2 * itemCount + 1; - } - const realIndex = keyToIndex.value[key] ?? 0; const orderZIndex = isStackingOrderDesc ? itemCount - realIndex - 1 : realIndex; + // Keep the order zIndex until the item is dragged (#417) + if (!activeItemBroughtToFront.value) { + return orderZIndex; + } + + if (activeItemKey.value === key) { + return 2 * itemCount + 1; + } + if (activationAnimationProgress.value > 0) { if (prevActiveItemKey.value === key) { return 2 * itemCount; diff --git a/packages/react-native-sortables/src/types/providers/shared.ts b/packages/react-native-sortables/src/types/providers/shared.ts index 5c4bcc9a..9a126762 100644 --- a/packages/react-native-sortables/src/types/providers/shared.ts +++ b/packages/react-native-sortables/src/types/providers/shared.ts @@ -87,6 +87,8 @@ export type CommonValuesContextType = activeAnimationProgress: SharedValue; inactiveAnimationProgress: SharedValue; activeItemDropped: SharedValue; + // True once the active item is dragged (not just long pressed) + activeItemBroughtToFront: SharedValue; // OTHER containerRef: AnimatedRef; @@ -154,6 +156,9 @@ export type ItemContextType = Simplify< Pick & { itemKey: string; isActive: SharedValue; + // True once the active item is dragged (moved), not just long pressed. + // Use it to disable interactive children while dragging. + isDragging: SharedValue; activationAnimationProgress: SharedValue; } > & { diff --git a/yarn.lock b/yarn.lock index e887683b..8878a367 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1975,6 +1975,13 @@ __metadata: languageName: node linkType: hard +"@dominicstop/ts-event-emitter@npm:^1.1.0": + version: 1.1.0 + resolution: "@dominicstop/ts-event-emitter@npm:1.1.0" + checksum: 10c0/fe16c03132736a0a798c232636ab6b127fd8063509511ccafbd99405c224a106cfb7a8d4af13c4f6f412ec0ea3baae3678aa2c8c09d81c9985c423d476404e1a + languageName: node + linkType: hard + "@effect/schema@npm:0.69.0": version: 0.69.0 resolution: "@effect/schema@npm:0.69.0" @@ -2641,6 +2648,44 @@ __metadata: languageName: node linkType: hard +"@floating-ui/core@npm:^1.7.5": + version: 1.7.5 + resolution: "@floating-ui/core@npm:1.7.5" + dependencies: + "@floating-ui/utils": "npm:^0.2.11" + checksum: 10c0/f9c52205e198b231d63a387b09c659aab08c46a1899e0b0bbe147b8b4f048b546f15ba17cb5d2a471da9534f1883d979425e13e5c4ceee67be63e4b0abd4db5d + languageName: node + linkType: hard + +"@floating-ui/dom@npm:^1.7.6": + version: 1.7.6 + resolution: "@floating-ui/dom@npm:1.7.6" + dependencies: + "@floating-ui/core": "npm:^1.7.5" + "@floating-ui/utils": "npm:^0.2.11" + checksum: 10c0/5c098e0d7b58c9bc769f276cca1766994c2c9c70c92d091a61bba8b3e9be53c011e0a79a8457fc2fb2f3d91697a26eb52e0a4962ef936dc963b45f58613c212f + languageName: node + linkType: hard + +"@floating-ui/react-dom@npm:^2.0.0": + version: 2.1.8 + resolution: "@floating-ui/react-dom@npm:2.1.8" + dependencies: + "@floating-ui/dom": "npm:^1.7.6" + peerDependencies: + react: ">=16.8.0" + react-dom: ">=16.8.0" + checksum: 10c0/26260ca4bb23b57c73b824062505abf977a008ce6e0463bdacca74f7e49853c4cd1d2bbf1a77c6caa17fa37dfffda2c6c4cd07a8737ebd7474aaff7818401d75 + languageName: node + linkType: hard + +"@floating-ui/utils@npm:^0.2.11": + version: 0.2.11 + resolution: "@floating-ui/utils@npm:0.2.11" + checksum: 10c0/f4bcea1559bdbb721ecc8e8ead423ac58d6a5b6e70b602cf0810ba6ad4ed1c77211b207faa88b278a9042f0c743133de08a203ed6741c1b6443423332884d5b3 + languageName: node + linkType: hard + "@fortawesome/fontawesome-common-types@npm:6.5.2": version: 6.5.2 resolution: "@fortawesome/fontawesome-common-types@npm:6.5.2" @@ -3604,6 +3649,471 @@ __metadata: languageName: node linkType: hard +"@radix-ui/primitive@npm:1.1.4": + version: 1.1.4 + resolution: "@radix-ui/primitive@npm:1.1.4" + checksum: 10c0/f366fa15b721a466ad78f9b5b966f7129fb6932f6f33ab117253ce8c6a13f4895dce4a1fd483d3f15859623d3bfd964a4b172fe1d6ccc3a1a3c4de4c2b861119 + languageName: node + linkType: hard + +"@radix-ui/react-arrow@npm:1.1.11": + version: 1.1.11 + resolution: "@radix-ui/react-arrow@npm:1.1.11" + dependencies: + "@radix-ui/react-primitive": "npm:2.1.7" + peerDependencies: + "@types/react": "*" + "@types/react-dom": "*" + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + "@types/react": + optional: true + "@types/react-dom": + optional: true + checksum: 10c0/d8db1fc66022057fbdb02f4a20c747706f9b9dc7e6966bd83a0c7d1aa81f27cbcfa48deb9bd86c2714f16d9c813868240bb584473e3344beb436ce4e03f6bbaf + languageName: node + linkType: hard + +"@radix-ui/react-collection@npm:1.1.11": + version: 1.1.11 + resolution: "@radix-ui/react-collection@npm:1.1.11" + dependencies: + "@radix-ui/react-compose-refs": "npm:1.1.3" + "@radix-ui/react-context": "npm:1.1.4" + "@radix-ui/react-primitive": "npm:2.1.7" + "@radix-ui/react-slot": "npm:1.3.0" + peerDependencies: + "@types/react": "*" + "@types/react-dom": "*" + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + "@types/react": + optional: true + "@types/react-dom": + optional: true + checksum: 10c0/529325b57e5ec347c91d19e23e94bb0a78d01f8cf1f4f74bfc7683467656bb05f82b60e51937e7b17c89d58a129170e6825ce383dfc8408a1de9ae120cbdfe53 + languageName: node + linkType: hard + +"@radix-ui/react-compose-refs@npm:1.1.3": + version: 1.1.3 + resolution: "@radix-ui/react-compose-refs@npm:1.1.3" + peerDependencies: + "@types/react": "*" + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + "@types/react": + optional: true + checksum: 10c0/c4853e7bc15cda6f68a1bbd7910412cc7f298419ee363fb4e40494e16a1cfadc857b2384dbe4d98b58c1312f280ca3474f67dc14da325d29ae8b7ebcfddaf743 + languageName: node + linkType: hard + +"@radix-ui/react-context-menu@npm:^2.0.1": + version: 2.3.2 + resolution: "@radix-ui/react-context-menu@npm:2.3.2" + dependencies: + "@radix-ui/primitive": "npm:1.1.4" + "@radix-ui/react-context": "npm:1.1.4" + "@radix-ui/react-menu": "npm:2.1.19" + "@radix-ui/react-primitive": "npm:2.1.7" + "@radix-ui/react-use-controllable-state": "npm:1.2.3" + peerDependencies: + "@types/react": "*" + "@types/react-dom": "*" + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + "@types/react": + optional: true + "@types/react-dom": + optional: true + checksum: 10c0/ae423e564cdf03412e89ea4fb9b50c3b15a7b5048908b431ff3787744abb4f6ea72c9053c48ef36cb5cfeac10e6731da64d9170b2d18ef308d7b8762f0669cc0 + languageName: node + linkType: hard + +"@radix-ui/react-context@npm:1.1.4": + version: 1.1.4 + resolution: "@radix-ui/react-context@npm:1.1.4" + peerDependencies: + "@types/react": "*" + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + "@types/react": + optional: true + checksum: 10c0/917be4dcb9fd9f465ab18f6982879daf83a5ca298a22504fa3bc4abd8dea963a57abb9ad38c099dd756ba2cddff0edde680bf8369012f44dedede20912702c8f + languageName: node + linkType: hard + +"@radix-ui/react-direction@npm:1.1.2": + version: 1.1.2 + resolution: "@radix-ui/react-direction@npm:1.1.2" + peerDependencies: + "@types/react": "*" + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + "@types/react": + optional: true + checksum: 10c0/e8172f8598b5bddaf82bdc2e16e9561d0d89eaf85ef3cd1fe2506a1564398ee6cb07b22fd7d0ac892e733d4f6247b1e328c4b3680141d22a04e262f14ff11230 + languageName: node + linkType: hard + +"@radix-ui/react-dismissable-layer@npm:1.1.14": + version: 1.1.14 + resolution: "@radix-ui/react-dismissable-layer@npm:1.1.14" + dependencies: + "@radix-ui/primitive": "npm:1.1.4" + "@radix-ui/react-compose-refs": "npm:1.1.3" + "@radix-ui/react-primitive": "npm:2.1.7" + "@radix-ui/react-use-callback-ref": "npm:1.1.2" + "@radix-ui/react-use-effect-event": "npm:0.0.3" + peerDependencies: + "@types/react": "*" + "@types/react-dom": "*" + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + "@types/react": + optional: true + "@types/react-dom": + optional: true + checksum: 10c0/371ec716e2029c503739a9efe2f85afca8d158474023a670fc546a1104824c5d78fcaba1484c652832fa006050fde2a8b52c851cd41d02cf8d564d8937aec027 + languageName: node + linkType: hard + +"@radix-ui/react-dropdown-menu@npm:^2.0.1": + version: 2.1.19 + resolution: "@radix-ui/react-dropdown-menu@npm:2.1.19" + dependencies: + "@radix-ui/primitive": "npm:1.1.4" + "@radix-ui/react-compose-refs": "npm:1.1.3" + "@radix-ui/react-context": "npm:1.1.4" + "@radix-ui/react-id": "npm:1.1.2" + "@radix-ui/react-menu": "npm:2.1.19" + "@radix-ui/react-primitive": "npm:2.1.7" + "@radix-ui/react-use-controllable-state": "npm:1.2.3" + peerDependencies: + "@types/react": "*" + "@types/react-dom": "*" + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + "@types/react": + optional: true + "@types/react-dom": + optional: true + checksum: 10c0/901a290f3542163403dd6d5439cc43c37ee8e8b3949e3b017784660f6c5bf477165d9fb5f483f52fd5914d72aa84275778312502257ce045b8c6fd553f758234 + languageName: node + linkType: hard + +"@radix-ui/react-focus-guards@npm:1.1.4": + version: 1.1.4 + resolution: "@radix-ui/react-focus-guards@npm:1.1.4" + peerDependencies: + "@types/react": "*" + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + "@types/react": + optional: true + checksum: 10c0/0447a97a2672ad04fa73e0af3a09adafa1e85327c43cec2ae7267daa8544c9e6ef3136fbadcbdd3e28bf1ff0864537241c159e26cf5800b7698e5e69772e7ed3 + languageName: node + linkType: hard + +"@radix-ui/react-focus-scope@npm:1.1.11": + version: 1.1.11 + resolution: "@radix-ui/react-focus-scope@npm:1.1.11" + dependencies: + "@radix-ui/react-compose-refs": "npm:1.1.3" + "@radix-ui/react-primitive": "npm:2.1.7" + "@radix-ui/react-use-callback-ref": "npm:1.1.2" + peerDependencies: + "@types/react": "*" + "@types/react-dom": "*" + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + "@types/react": + optional: true + "@types/react-dom": + optional: true + checksum: 10c0/e773dfbe4c3907067db4d96082bf9a4dad730ad7396dc181f09fc79e054c4a7382dee6a3b948efef97904ae0387dc81b7733e479af69f5a22de2649c99847561 + languageName: node + linkType: hard + +"@radix-ui/react-id@npm:1.1.2": + version: 1.1.2 + resolution: "@radix-ui/react-id@npm:1.1.2" + dependencies: + "@radix-ui/react-use-layout-effect": "npm:1.1.2" + peerDependencies: + "@types/react": "*" + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + "@types/react": + optional: true + checksum: 10c0/cd8638f0e259b9cee26cc8248b96533927abf91302a9164d674a45119a15dbb6929652e450c18bc3b99adf5d02a698152bf45d3ae052e1cd63f68fe8dd1ca87d + languageName: node + linkType: hard + +"@radix-ui/react-menu@npm:2.1.19": + version: 2.1.19 + resolution: "@radix-ui/react-menu@npm:2.1.19" + dependencies: + "@radix-ui/primitive": "npm:1.1.4" + "@radix-ui/react-collection": "npm:1.1.11" + "@radix-ui/react-compose-refs": "npm:1.1.3" + "@radix-ui/react-context": "npm:1.1.4" + "@radix-ui/react-direction": "npm:1.1.2" + "@radix-ui/react-dismissable-layer": "npm:1.1.14" + "@radix-ui/react-focus-guards": "npm:1.1.4" + "@radix-ui/react-focus-scope": "npm:1.1.11" + "@radix-ui/react-id": "npm:1.1.2" + "@radix-ui/react-popper": "npm:1.3.2" + "@radix-ui/react-portal": "npm:1.1.13" + "@radix-ui/react-presence": "npm:1.1.6" + "@radix-ui/react-primitive": "npm:2.1.7" + "@radix-ui/react-roving-focus": "npm:1.1.14" + "@radix-ui/react-slot": "npm:1.3.0" + "@radix-ui/react-use-callback-ref": "npm:1.1.2" + aria-hidden: "npm:^1.2.4" + react-remove-scroll: "npm:^2.7.2" + peerDependencies: + "@types/react": "*" + "@types/react-dom": "*" + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + "@types/react": + optional: true + "@types/react-dom": + optional: true + checksum: 10c0/25a625a2d0e08b60ce29c015f19c582e2729f25321c07b03b9b2ffe0a538628fbea50353f9d447077c3c224b09c21cda64d6e24e8cbbdb52d041b5cd6b0253ca + languageName: node + linkType: hard + +"@radix-ui/react-popper@npm:1.3.2": + version: 1.3.2 + resolution: "@radix-ui/react-popper@npm:1.3.2" + dependencies: + "@floating-ui/react-dom": "npm:^2.0.0" + "@radix-ui/react-arrow": "npm:1.1.11" + "@radix-ui/react-compose-refs": "npm:1.1.3" + "@radix-ui/react-context": "npm:1.1.4" + "@radix-ui/react-primitive": "npm:2.1.7" + "@radix-ui/react-use-callback-ref": "npm:1.1.2" + "@radix-ui/react-use-layout-effect": "npm:1.1.2" + "@radix-ui/react-use-rect": "npm:1.1.2" + "@radix-ui/react-use-size": "npm:1.1.2" + "@radix-ui/rect": "npm:1.1.2" + peerDependencies: + "@types/react": "*" + "@types/react-dom": "*" + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + "@types/react": + optional: true + "@types/react-dom": + optional: true + checksum: 10c0/84d6ed24c0fe36acecd09b7b8c3343eaa2bdc7500cf28dec18c8236d4ebb12d7df48df2b866232bd035ccbc42be57fff1144454ae9cab6a6a3a75b073e9d022d + languageName: node + linkType: hard + +"@radix-ui/react-portal@npm:1.1.13": + version: 1.1.13 + resolution: "@radix-ui/react-portal@npm:1.1.13" + dependencies: + "@radix-ui/react-primitive": "npm:2.1.7" + "@radix-ui/react-use-layout-effect": "npm:1.1.2" + peerDependencies: + "@types/react": "*" + "@types/react-dom": "*" + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + "@types/react": + optional: true + "@types/react-dom": + optional: true + checksum: 10c0/3a158e4e9c4360dec0ff4303be2d3eead0c4438c8c240194a6116901cd8fcf98e22bcd65f33caaa778556cc434a5048b16a8deea82b60eb8f487f36f67fecf06 + languageName: node + linkType: hard + +"@radix-ui/react-presence@npm:1.1.6": + version: 1.1.6 + resolution: "@radix-ui/react-presence@npm:1.1.6" + dependencies: + "@radix-ui/react-use-layout-effect": "npm:1.1.2" + peerDependencies: + "@types/react": "*" + "@types/react-dom": "*" + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + "@types/react": + optional: true + "@types/react-dom": + optional: true + checksum: 10c0/8b96ba32eae20162005f7e818b07e1e6e351da03f4753a79403ec58d27c4965e881a506960283fd7ded5b8ecf5ccb6a58bbc1d6799f5254a6cd4e5ae8837e345 + languageName: node + linkType: hard + +"@radix-ui/react-primitive@npm:2.1.7": + version: 2.1.7 + resolution: "@radix-ui/react-primitive@npm:2.1.7" + dependencies: + "@radix-ui/react-slot": "npm:1.3.0" + peerDependencies: + "@types/react": "*" + "@types/react-dom": "*" + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + "@types/react": + optional: true + "@types/react-dom": + optional: true + checksum: 10c0/0cb8711df46447966f32752ca10237a635a5207e75e73f716cc740b418aa4a1089fce342c9741e3c727274468dea205cee3b8a4433a80100bfdb624b7ba4ade6 + languageName: node + linkType: hard + +"@radix-ui/react-roving-focus@npm:1.1.14": + version: 1.1.14 + resolution: "@radix-ui/react-roving-focus@npm:1.1.14" + dependencies: + "@radix-ui/primitive": "npm:1.1.4" + "@radix-ui/react-collection": "npm:1.1.11" + "@radix-ui/react-compose-refs": "npm:1.1.3" + "@radix-ui/react-context": "npm:1.1.4" + "@radix-ui/react-direction": "npm:1.1.2" + "@radix-ui/react-id": "npm:1.1.2" + "@radix-ui/react-primitive": "npm:2.1.7" + "@radix-ui/react-use-callback-ref": "npm:1.1.2" + "@radix-ui/react-use-controllable-state": "npm:1.2.3" + peerDependencies: + "@types/react": "*" + "@types/react-dom": "*" + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + "@types/react": + optional: true + "@types/react-dom": + optional: true + checksum: 10c0/76be4ec8ba0c86b6115be02d2b5377af78f2871a323199887f1746ae5e7937656e9e4a79ceff08f4ee0984a222de5b343444d989e424ea56cf1a2a52cd8a6969 + languageName: node + linkType: hard + +"@radix-ui/react-slot@npm:1.3.0": + version: 1.3.0 + resolution: "@radix-ui/react-slot@npm:1.3.0" + dependencies: + "@radix-ui/react-compose-refs": "npm:1.1.3" + peerDependencies: + "@types/react": "*" + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + "@types/react": + optional: true + checksum: 10c0/7353520950dcbc5c331f79aeac5bc8efa5130014e8121f85b337996ec5a2cef89f1375ff879b72ba05182de93c1126b22ec328f171edd7a2888c1532d10f74b5 + languageName: node + linkType: hard + +"@radix-ui/react-use-callback-ref@npm:1.1.2": + version: 1.1.2 + resolution: "@radix-ui/react-use-callback-ref@npm:1.1.2" + peerDependencies: + "@types/react": "*" + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + "@types/react": + optional: true + checksum: 10c0/aa43df8cf54b410f2b0fff817247cee5e4d4560b86d9bff7c17c19441726163c28f09f908e154607e5e6d0a055538c768381b723c9f5d1019807546f352b4cc1 + languageName: node + linkType: hard + +"@radix-ui/react-use-controllable-state@npm:1.2.3": + version: 1.2.3 + resolution: "@radix-ui/react-use-controllable-state@npm:1.2.3" + dependencies: + "@radix-ui/react-use-effect-event": "npm:0.0.3" + "@radix-ui/react-use-layout-effect": "npm:1.1.2" + peerDependencies: + "@types/react": "*" + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + "@types/react": + optional: true + checksum: 10c0/8c551263a2753cebc1d60019d8beb307d20cbb1b9847887a9eed13db5392672c6d5179b7f46a2cbd34a64bda702617670012ebcd32cffeb22f6645fcf5345ee8 + languageName: node + linkType: hard + +"@radix-ui/react-use-effect-event@npm:0.0.3": + version: 0.0.3 + resolution: "@radix-ui/react-use-effect-event@npm:0.0.3" + dependencies: + "@radix-ui/react-use-layout-effect": "npm:1.1.2" + peerDependencies: + "@types/react": "*" + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + "@types/react": + optional: true + checksum: 10c0/8808fab0b26e30da9b50070a426b3ac135132c360b23a2a5de331bd06d8b164b10cd5561573dd17530172ba67dde428057fbf9a1f5fae2cba3cc66f118e72406 + languageName: node + linkType: hard + +"@radix-ui/react-use-layout-effect@npm:1.1.2": + version: 1.1.2 + resolution: "@radix-ui/react-use-layout-effect@npm:1.1.2" + peerDependencies: + "@types/react": "*" + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + "@types/react": + optional: true + checksum: 10c0/c7fc76744a4d37b5010eac33eda6439db2ac4d657e2bb5f596236e60aa4384ac88a7b2e3ebc1c88312c5c536a40081cc85ae6d70c0bba4bb8702d302b8ff07f7 + languageName: node + linkType: hard + +"@radix-ui/react-use-rect@npm:1.1.2": + version: 1.1.2 + resolution: "@radix-ui/react-use-rect@npm:1.1.2" + dependencies: + "@radix-ui/rect": "npm:1.1.2" + peerDependencies: + "@types/react": "*" + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + "@types/react": + optional: true + checksum: 10c0/eafaa88ea33bfb3761a81a3badb73ecf99c613aa7c10e53dac08fee3457d21f4dfd1e65b5a47e7925e4aa1da60f2793404183353d4d21e85aae4009c76de64d4 + languageName: node + linkType: hard + +"@radix-ui/react-use-size@npm:1.1.2": + version: 1.1.2 + resolution: "@radix-ui/react-use-size@npm:1.1.2" + dependencies: + "@radix-ui/react-use-layout-effect": "npm:1.1.2" + peerDependencies: + "@types/react": "*" + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + "@types/react": + optional: true + checksum: 10c0/0a088412bb831fd1d59f9058352aca384c2d0a07b894b35707b3486eea4a1d8a37a04fb117379f9a8a46a921b0092f97b29b517689c8f97029ec73eb68973521 + languageName: node + linkType: hard + +"@radix-ui/rect@npm:1.1.2": + version: 1.1.2 + resolution: "@radix-ui/rect@npm:1.1.2" + checksum: 10c0/304d648c4b6786755a10eabf2327f40b7c6303bb588174ab6194a5bb032c195c51f3e43395dee1dd37239fa68b63558092f2bc8ce5a14962d5e4303afb0a17ca + languageName: node + linkType: hard + "@react-native-async-storage/async-storage@npm:2.2.0": version: 2.2.0 resolution: "@react-native-async-storage/async-storage@npm:2.2.0" @@ -3793,6 +4303,16 @@ __metadata: languageName: node linkType: hard +"@react-native-menu/menu@npm:2.0.0": + version: 2.0.0 + resolution: "@react-native-menu/menu@npm:2.0.0" + peerDependencies: + react: "*" + react-native: "*" + checksum: 10c0/1e88e1892730c70e28a654a8df6427f80f1b88880eaa62f2744072b9cebead4e834490d15ef7f0b91eadf9cb1ec9241b29f116ce7e94661685b41faf10af3fa3 + languageName: node + linkType: hard + "@react-native/assets-registry@npm:0.81.4": version: 0.81.4 resolution: "@react-native/assets-registry@npm:0.81.4" @@ -5577,6 +6097,15 @@ __metadata: languageName: node linkType: hard +"aria-hidden@npm:^1.2.4": + version: 1.2.6 + resolution: "aria-hidden@npm:1.2.6" + dependencies: + tslib: "npm:^2.0.0" + checksum: 10c0/7720cb539497a9f760f68f98a4b30f22c6767aa0e72fa7d58279f7c164e258fc38b2699828f8de881aab0fc8e9c56d1313a3f1a965046fc0381a554dbc72b54a + languageName: node + linkType: hard + "arktype@npm:^2.1.15": version: 2.1.20 resolution: "arktype@npm:2.1.20" @@ -7394,6 +7923,13 @@ __metadata: languageName: node linkType: hard +"detect-node-es@npm:^1.1.0": + version: 1.1.0 + resolution: "detect-node-es@npm:1.1.0" + checksum: 10c0/e562f00de23f10c27d7119e1af0e7388407eb4b06596a25f6d79a360094a109ff285de317f02b090faae093d314cf6e73ac3214f8a5bb3a0def5bece94557fbe + languageName: node + linkType: hard + "detective-amd@npm:^6.0.0": version: 6.0.0 resolution: "detective-amd@npm:6.0.0" @@ -8439,6 +8975,7 @@ __metadata: "@react-native-community/cli": "npm:20.2.0" "@react-native-community/cli-platform-android": "npm:20.2.0" "@react-native-community/cli-platform-ios": "npm:20.2.0" + "@react-native-menu/menu": "npm:2.0.0" "@react-native/babel-preset": "npm:0.86.0" "@react-native/jest-preset": "npm:0.86.0" "@react-native/metro-config": "npm:0.86.0" @@ -8458,8 +8995,11 @@ __metadata: prettier: "npm:^3.3.2" react: "npm:19.2.3" react-native: "npm:0.86.0" + react-native-context-menu-view: "npm:1.21.0" react-native-gesture-handler: "npm:2.32.0" react-native-haptic-feedback: "npm:2.3.3" + react-native-ios-context-menu: "npm:3.1.0" + react-native-ios-utilities: "npm:5.1.2" react-native-reanimated: "npm:4.5.1" react-native-safe-area-context: "npm:5.7.0" react-native-screens: "npm:4.25.2" @@ -8468,6 +9008,7 @@ __metadata: react-native-worklets: "npm:0.10.1" react-test-renderer: "npm:19.2.3" typescript: "npm:^5.8.3" + zeego: "npm:3.0.6" languageName: unknown linkType: soft @@ -9363,6 +9904,13 @@ __metadata: languageName: node linkType: hard +"get-nonce@npm:^1.0.0": + version: 1.0.1 + resolution: "get-nonce@npm:1.0.1" + checksum: 10c0/2d7df55279060bf0568549e1ffc9b84bc32a32b7541675ca092dce56317cdd1a59a98dcc4072c9f6a980779440139a3221d7486f52c488e69dc0fd27b1efb162 + languageName: node + linkType: hard + "get-own-enumerable-property-symbols@npm:^3.0.0": version: 3.0.2 resolution: "get-own-enumerable-property-symbols@npm:3.0.2" @@ -14888,6 +15436,16 @@ __metadata: languageName: node linkType: hard +"react-native-context-menu-view@npm:1.21.0": + version: 1.21.0 + resolution: "react-native-context-menu-view@npm:1.21.0" + peerDependencies: + react: ^16.8.1 || ^17.0.0 || ^18.0.0 || ^19.0.0 + react-native: ">=0.60.0-rc.0 <1.0.x" + checksum: 10c0/923a11c034da814ac24402af0308b219a319f8b22d9a0ca381cf820f3f863c15bc08fa3c6107cdc7127004f5ba0096374a764b511edb664acb53a9212b68ae26 + languageName: node + linkType: hard + "react-native-gesture-handler@npm:2.28.0": version: 2.28.0 resolution: "react-native-gesture-handler@npm:2.28.0" @@ -14939,6 +15497,29 @@ __metadata: languageName: node linkType: hard +"react-native-ios-context-menu@npm:3.1.0": + version: 3.1.0 + resolution: "react-native-ios-context-menu@npm:3.1.0" + dependencies: + "@dominicstop/ts-event-emitter": "npm:^1.1.0" + peerDependencies: + react: "*" + react-native: "*" + react-native-ios-utilities: "*" + checksum: 10c0/7dda8d8ee2b0871b9c1bfce985bce49b8762988b34c5af6679fcdf47f125e98df7c1159245f7ca4070a00fd9874336a4fa17f8bf582377e849a850eb511d7680 + languageName: node + linkType: hard + +"react-native-ios-utilities@npm:5.1.2": + version: 5.1.2 + resolution: "react-native-ios-utilities@npm:5.1.2" + peerDependencies: + react: "*" + react-native: "*" + checksum: 10c0/b1b700faa2f447423cbfbe941a5e0eaeb7357df5f85f0a5d27a717f040174ecc3f972007f39e76cd053e4d90248028b137726524d71756e3a051efa32136754d + languageName: node + linkType: hard + "react-native-is-edge-to-edge@npm:1.1.7": version: 1.1.7 resolution: "react-native-is-edge-to-edge@npm:1.1.7" @@ -15321,6 +15902,57 @@ __metadata: languageName: node linkType: hard +"react-remove-scroll-bar@npm:^2.3.7": + version: 2.3.8 + resolution: "react-remove-scroll-bar@npm:2.3.8" + dependencies: + react-style-singleton: "npm:^2.2.2" + tslib: "npm:^2.0.0" + peerDependencies: + "@types/react": "*" + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + "@types/react": + optional: true + checksum: 10c0/9a0675c66cbb52c325bdbfaed80987a829c4504cefd8ff2dd3b6b3afc9a1500b8ec57b212e92c1fb654396d07bbe18830a8146fe77677d2a29ce40b5e1f78654 + languageName: node + linkType: hard + +"react-remove-scroll@npm:^2.7.2": + version: 2.7.2 + resolution: "react-remove-scroll@npm:2.7.2" + dependencies: + react-remove-scroll-bar: "npm:^2.3.7" + react-style-singleton: "npm:^2.2.3" + tslib: "npm:^2.1.0" + use-callback-ref: "npm:^1.3.3" + use-sidecar: "npm:^1.1.3" + peerDependencies: + "@types/react": "*" + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc + peerDependenciesMeta: + "@types/react": + optional: true + checksum: 10c0/b5f3315bead75e72853f492c0b51ba8fb4fa09a4534d7fc42d6fcd59ca3e047cf213279ffc1e35b337e314ef5a04cb2b12544c85e0078802271731c27c09e5aa + languageName: node + linkType: hard + +"react-style-singleton@npm:^2.2.2, react-style-singleton@npm:^2.2.3": + version: 2.2.3 + resolution: "react-style-singleton@npm:2.2.3" + dependencies: + get-nonce: "npm:^1.0.0" + tslib: "npm:^2.0.0" + peerDependencies: + "@types/react": "*" + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc + peerDependenciesMeta: + "@types/react": + optional: true + checksum: 10c0/841938ff16d16a6b76895f4cb2e1fea957e5fe3b30febbf03a54892dae1c9153f2383e231dea0b3ba41192ad2f2849448fa859caccd288943bce32639e971bee + languageName: node + linkType: hard + "react-test-renderer@npm:19.2.3": version: 19.2.3 resolution: "react-test-renderer@npm:19.2.3" @@ -16219,6 +16851,13 @@ __metadata: languageName: node linkType: hard +"sf-symbols-typescript@npm:^2.0.0": + version: 2.2.0 + resolution: "sf-symbols-typescript@npm:2.2.0" + checksum: 10c0/3f3bbf33aaad19e619d6f169899b39e9fe9c5fd21f0d6d511100e36887606ad349109ddc6ff82933f2b8cbf437dd7105c2ae6b0059b291dc47f143b30c2074cc + languageName: node + linkType: hard + "shebang-command@npm:^2.0.0": version: 2.0.0 resolution: "shebang-command@npm:2.0.0" @@ -17468,7 +18107,7 @@ __metadata: languageName: node linkType: hard -"tslib@npm:2.8.1, tslib@npm:^2.1.0, tslib@npm:^2.4.0": +"tslib@npm:2.8.1, tslib@npm:^2.0.0, tslib@npm:^2.1.0, tslib@npm:^2.4.0": version: 2.8.1 resolution: "tslib@npm:2.8.1" checksum: 10c0/9c4759110a19c53f992d9aae23aac5ced636e99887b51b9e61def52611732872ff7668757d4e4c61f19691e36f4da981cd9485e869b4a7408d689f6bf1f14e62 @@ -17916,6 +18555,21 @@ __metadata: languageName: node linkType: hard +"use-callback-ref@npm:^1.3.3": + version: 1.3.3 + resolution: "use-callback-ref@npm:1.3.3" + dependencies: + tslib: "npm:^2.0.0" + peerDependencies: + "@types/react": "*" + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc + peerDependenciesMeta: + "@types/react": + optional: true + checksum: 10c0/f887488c6e6075cdad4962979da1714b217bcb1ee009a9e57ce9a844bcfc4c3a99e93983dfc2e5af9e0913824d24e730090ff255e902c516dcb58d2d3837e01c + languageName: node + linkType: hard + "use-latest-callback@npm:^0.2.1": version: 0.2.1 resolution: "use-latest-callback@npm:0.2.1" @@ -17925,6 +18579,22 @@ __metadata: languageName: node linkType: hard +"use-sidecar@npm:^1.1.3": + version: 1.1.3 + resolution: "use-sidecar@npm:1.1.3" + dependencies: + detect-node-es: "npm:^1.1.0" + tslib: "npm:^2.0.0" + peerDependencies: + "@types/react": "*" + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc + peerDependenciesMeta: + "@types/react": + optional: true + checksum: 10c0/161599bf921cfaa41c85d2b01c871975ee99260f3e874c2d41c05890d41170297bdcf314bc5185e7a700de2034ac5b888e3efc8e9f35724f4918f53538d717c9 + languageName: node + linkType: hard + "use-sync-external-store@npm:^1.2.2": version: 1.4.0 resolution: "use-sync-external-store@npm:1.4.0" @@ -18485,6 +19155,23 @@ __metadata: languageName: node linkType: hard +"zeego@npm:3.0.6": + version: 3.0.6 + resolution: "zeego@npm:3.0.6" + dependencies: + "@radix-ui/react-context-menu": "npm:^2.0.1" + "@radix-ui/react-dropdown-menu": "npm:^2.0.1" + sf-symbols-typescript: "npm:^2.0.0" + peerDependencies: + "@react-native-menu/menu": 1.2.2 + react: "*" + react-native: "*" + react-native-ios-context-menu: 3.1.0 + react-native-ios-utilities: 5.1.2 + checksum: 10c0/76eec9ca0cee085da0bb50188ee08d6d67e3b2b2ea17b0a30e34fb3e39cd67a75d2650bdd4f5c593aec89cd778622d083dd20c61fd6adebe5ca01d694ac30fd5 + languageName: node + linkType: hard + "zod-validation-error@npm:^3.0.3": version: 3.5.0 resolution: "zod-validation-error@npm:3.5.0"