Skip to content

Commit aca7b3c

Browse files
fix: mirror split pane replace into web URL
1 parent 9e97403 commit aca7b3c

1 file changed

Lines changed: 86 additions & 6 deletions

File tree

src/SplitView.tsx

Lines changed: 86 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -280,6 +280,65 @@ function WideSplitView({
280280
// (Settings: drill into a sub-form). Browser Back/Forward leave intent null
281281
// and re-derive from the pane crumb trail or a replace.
282282
const applyIntentRef = React.useRef<'replace' | 'push' | null>(null);
283+
// Selection href the pane is already showing, so the URL-sync effect below can
284+
// tell "the URL changed" from "we changed the URL to match the pane".
285+
const appliedHrefRef = React.useRef<string | undefined>(undefined);
286+
287+
// `replace()` inside the pane (create screens swapping themselves for the
288+
// record they just created) goes through `navigateLink`, not `navigate`. In
289+
// selectionParam mode the pane is a mirror of the main URL, so applying such a
290+
// link to the pane alone leaves the address bar on the screen the user just
291+
// left: reloading a freshly created entity lands back on its empty "new" form.
292+
//
293+
// Apply the link to the pane exactly as before, then mirror the scene it
294+
// resolves to into the main URL. `appliedHrefRef` is primed so the sync effect
295+
// recognises the pane as already up to date and does not re-derive it.
296+
const navigateLinkMirroringUrl = React.useCallback(
297+
(url: string, historyAction?: 'add' | 'replace' | 'none') => {
298+
if (!selectionParam) {
299+
detailNavigator.navigateLink(url, historyAction);
300+
return;
301+
}
302+
let href: string | undefined;
303+
try {
304+
const parsed = detailNavigator.parseLink(url);
305+
href =
306+
parsed?.state?.key && parsed.state.key !== rootKey
307+
? encodeSelectionHref(parsed.state.key, parsed.data)
308+
: undefined;
309+
} catch {
310+
// Unresolvable link: let the pane deal with it and leave the URL alone.
311+
detailNavigator.navigateLink(url, historyAction);
312+
return;
313+
}
314+
detailNavigator.navigateLink(url, historyAction);
315+
appliedHrefRef.current = href;
316+
// A replaced screen must not stay reachable through in-app back either, so
317+
// drop its selection-history entry before the effect records the new one —
318+
// that keeps this stack in step with the browser history it mirrors.
319+
if (historyAction === 'replace') {
320+
selectionStackRef.current.pop();
321+
}
322+
const currentData = mainNavigator.stateContext.data ?? {};
323+
const next: Record<string, any> = { ...currentData };
324+
if (href) {
325+
next[selectionParam] = href;
326+
} else {
327+
delete next[selectionParam];
328+
}
329+
mainNavigator.refresh(
330+
next,
331+
historyAction === 'replace' ? 'replace' : 'add'
332+
);
333+
},
334+
[
335+
detailNavigator,
336+
encodeSelectionHref,
337+
mainNavigator,
338+
rootKey,
339+
selectionParam,
340+
]
341+
);
283342

284343
// Pushes from the master column select a new detail: they reset the pane to
285344
// [root, detail] instead of stacking on whatever was selected before.
@@ -290,13 +349,20 @@ function WideSplitView({
290349
// below. That keeps ONE history timeline (URL + browser/native Back) and
291350
// avoids the pane driving itself out of sync with the URL.
292351
const masterNavigator = React.useMemo(() => {
293-
const selectViaUrl = (key: string, params?: any) => {
352+
const selectViaUrl = (
353+
key: string,
354+
params?: any,
355+
historyAction?: 'add' | 'replace' | 'none'
356+
) => {
294357
applyIntentRef.current = 'replace';
295358
const href = encodeSelectionHref(key, params);
296359
const currentData = mainNavigator.stateContext.data ?? {};
360+
if (historyAction === 'replace') {
361+
selectionStackRef.current.pop();
362+
}
297363
mainNavigator.refresh(
298364
{ ...currentData, [selectionParam as string]: href },
299-
'add'
365+
historyAction === 'replace' ? 'replace' : 'add'
300366
);
301367
};
302368
const selectDetail = (key: string, params?: any) => {
@@ -311,6 +377,9 @@ function WideSplitView({
311377
if (prop === 'navigate') {
312378
return selectionParam ? selectViaUrl : selectDetail;
313379
}
380+
if (selectionParam && prop === 'navigateLink') {
381+
return navigateLinkMirroringUrl;
382+
}
314383
// In selectionParam mode the selection (and any deeper drill pushed
315384
// inside the detail pane) lives on the MAIN navigator's history. In-app
316385
// back (BackLink/pop) must therefore step through that history — the
@@ -332,6 +401,7 @@ function WideSplitView({
332401
selectionParam,
333402
mainNavigator,
334403
encodeSelectionHref,
404+
navigateLinkMirroringUrl,
335405
paneGoBack,
336406
paneCanGoBack,
337407
]);
@@ -340,13 +410,20 @@ function WideSplitView({
340410
// record a main-URL history entry when selectionParam is set). Distinct from
341411
// masterNavigator, which always replaces.
342412
const detailStackNavigator = React.useMemo(() => {
343-
const stackViaUrl = (key: string, params?: any) => {
344-
applyIntentRef.current = 'push';
413+
const stackViaUrl = (
414+
key: string,
415+
params?: any,
416+
historyAction?: 'add' | 'replace' | 'none'
417+
) => {
418+
applyIntentRef.current = historyAction === 'replace' ? 'replace' : 'push';
345419
const href = encodeSelectionHref(key, params);
346420
const currentData = mainNavigator.stateContext.data ?? {};
421+
if (historyAction === 'replace') {
422+
selectionStackRef.current.pop();
423+
}
347424
mainNavigator.refresh(
348425
{ ...currentData, [selectionParam as string]: href },
349-
'add'
426+
historyAction === 'replace' ? 'replace' : 'add'
350427
);
351428
};
352429
return new Proxy(detailNavigator, {
@@ -357,6 +434,9 @@ function WideSplitView({
357434
: (key: string, params?: any) =>
358435
detailNavigator.navigate(key, params);
359436
}
437+
if (selectionParam && prop === 'navigateLink') {
438+
return navigateLinkMirroringUrl;
439+
}
360440
if (selectionParam && prop === 'navigateBack') {
361441
return paneGoBack;
362442
}
@@ -372,6 +452,7 @@ function WideSplitView({
372452
selectionParam,
373453
mainNavigator,
374454
encodeSelectionHref,
455+
navigateLinkMirroringUrl,
375456
paneGoBack,
376457
paneCanGoBack,
377458
]);
@@ -385,7 +466,6 @@ function WideSplitView({
385466
// Master taps REPLACE the pane (fluent reset → one screen). Detail drills
386467
// STACK (navigate on top of the existing crumb trail) so screens sit on top
387468
// of each other — iOS Settings style — while still being URL-history-backed.
388-
const appliedHrefRef = React.useRef<string | undefined>(undefined);
389469
React.useEffect(() => {
390470
if (!selectionParam) {
391471
return undefined;

0 commit comments

Comments
 (0)