From af3e38710f9c045bd5229000fe1a94a8724dd349 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 5 Aug 2026 13:07:35 +0200 Subject: [PATCH] feat!: close part of the stack, not only all of it MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit closeAll() could only empty a group, so a sheet deep in a stack had no way to say "close what is above me" or "close three". Both were expressible from the store already — stackOrderByGroup is ordered and a sheet knows its own id — so this is an API gap, not a missing capability. closeTo(id) close down to a sheet, leaving it open closeDepth(n) close n sheets from the top closeAbove() from useBottomSheetContext: everything above me All three route through closeAllAnimated with a resolved range, so stagger, onBeforeClose interception and stoppedAt reporting behave exactly as before. Passing both bounds takes whichever closes fewer sheets, so neither can widen the other. An `until` naming a sheet outside the group closes nothing — for a bounded call, silently emptying the stack is the worst possible fallback. BREAKING: CloseAllResult.closedAll is now .completed. Bounded by until/depth it means "closed the requested range", which the old name contradicted. CloseAllOptions is replaced by CascadeOptions, exported from the store. 11 tests cover the boundaries: depth 0 and negative, depth past the stack, an until that is the top sheet, an until that is absent, both bounds fighting in either direction, and an interceptor stopping a bounded cascade. --- example/src/screens/HomeScreen.tsx | 9 ++ example/src/sheets/PartialCloseSheets.tsx | 158 ++++++++++++++++++++++ example/src/sheets/index.ts | 1 + src/__tests__/coordinator.test.ts | 141 ++++++++++++++++++- src/bottomSheetCoordinator.ts | 51 ++++++- src/index.tsx | 2 +- src/store/types.ts | 29 +++- src/useBottomSheetContext.ts | 24 +++- src/useBottomSheetControl.ts | 7 +- src/useBottomSheetManager.tsx | 51 +++++-- 10 files changed, 443 insertions(+), 30 deletions(-) create mode 100644 example/src/sheets/PartialCloseSheets.tsx diff --git a/example/src/screens/HomeScreen.tsx b/example/src/screens/HomeScreen.tsx index 670e5ae..e2bc2dc 100644 --- a/example/src/screens/HomeScreen.tsx +++ b/example/src/screens/HomeScreen.tsx @@ -26,6 +26,7 @@ import { } from '../sheets'; import { colors, sharedStyles } from '../styles/theme'; import { CloseInterceptionDemo } from '../sheets/CloseInterceptionSheets'; +import { PartialCloseDemo } from '../sheets/PartialCloseSheets'; export function HomeScreen() { const { top } = useSafeAreaInsets(); @@ -167,6 +168,14 @@ export function HomeScreen() { persistentWithPortalControl.open({ scaleBackground: true }) } /> + + open(, { scaleBackground: true }) + } + /> ((_, ref) => ( + +)); + +interface LevelProps { + level: number; + /** ID of level 1, threaded down so deeper levels can aim `closeTo` at it. */ + rootId?: string; +} + +const PartialCloseLevel = forwardRef( + ({ level, rootId }, ref) => { + const { open, closeAll, closeTo, closeDepth } = useBottomSheetManager(); + const { id, close, closeAbove } = useBottomSheetContext(); + + const [lastResult, setLastResult] = useState('no call yet'); + const [inclusive, setInclusive] = useState(false); + const [blocking, setBlocking] = useState(false); + + // Lets a level refuse, so a cascade can be stopped mid-way and `stoppedAt` + // observed rather than taken on faith. + useOnBeforeClose(({ onCancel, onConfirm }) => + blocking ? onCancel() : onConfirm() + ); + + const target = rootId ?? id; + + const report = async (call: Promise) => + setLastResult(formatResult(await call)); + + const pushLevel = () => + open(, { + mode: 'push', + scaleBackground: true, + }); + + return ( + + + + {blocking ? : null} + + + Partial close · level {level} + + Push a few levels, then close part of the stack. + + + + LAST RESULT + {lastResult} + + + + Refuse to close (interceptor) + + + + inclusive + + + +