-
Notifications
You must be signed in to change notification settings - Fork 23
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #3330 from quantified-uncertainty/controlled-links
Draft improvements
- Loading branch information
Showing
23 changed files
with
329 additions
and
46 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,3 +1,12 @@ | ||
{ | ||
"extends": "next/core-web-vitals" | ||
"extends": "next/core-web-vitals", | ||
"rules": { | ||
"no-restricted-imports": [ | ||
"error", | ||
{ | ||
"name": "next/link", | ||
"message": "Please use src/components/ui/Link.tsx instead." | ||
} | ||
] | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
42 changes: 42 additions & 0 deletions
42
packages/hub/src/components/ExitConfirmationWrapper/ConfirmNavigationModal.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,42 @@ | ||
import { useRouter } from "next/navigation"; | ||
import { FC, use, useCallback } from "react"; | ||
|
||
import { Button, Modal } from "@quri/ui"; | ||
|
||
import { ExitConfirmationWrapperContext } from "./context"; | ||
|
||
export const ConfirmNavigationModal: FC = () => { | ||
const { state, dispatch } = use(ExitConfirmationWrapperContext); | ||
const close = useCallback(() => { | ||
dispatch({ type: "clearPendingLink" }); | ||
}, [dispatch]); | ||
|
||
const router = useRouter(); | ||
|
||
if (!state.pendingLink) { | ||
return null; | ||
} | ||
|
||
return ( | ||
<Modal close={close}> | ||
<Modal.Header>You have unsaved changes</Modal.Header> | ||
<Modal.Body> | ||
Are you sure you want to leave this page? You changes are not saved. | ||
</Modal.Body> | ||
<Modal.Footer> | ||
<div className="flex justify-end gap-2"> | ||
<Button onClick={() => close()}>Stay on this page</Button> | ||
<Button | ||
onClick={() => { | ||
router.push(state.pendingLink!); | ||
close(); | ||
}} | ||
theme="primary" | ||
> | ||
Continue | ||
</Button> | ||
</div> | ||
</Modal.Footer> | ||
</Modal> | ||
); | ||
}; |
70 changes: 70 additions & 0 deletions
70
packages/hub/src/components/ExitConfirmationWrapper/context.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,70 @@ | ||
import { createContext, Reducer } from "react"; | ||
|
||
type State = { | ||
// Checks are functions; this way we can check dynamic parameters such as | ||
// form values to decide whether navigation should be intercepted. | ||
// If any of the checks returns a true value, the exit confirmation will be shown. | ||
checks: Record<string, () => boolean>; | ||
// Link was intercepted, need to show a modal. | ||
pendingLink: string | undefined; | ||
}; | ||
|
||
type Action = | ||
| { | ||
type: "addExitConfirmationCheck"; | ||
payload: { | ||
key: string; | ||
check: () => boolean; | ||
}; | ||
} | ||
| { | ||
type: "removeExitConfirmationCheck"; | ||
payload: { | ||
key: string; | ||
}; | ||
} | ||
| { | ||
type: "intercept"; | ||
payload: { | ||
link: string; | ||
}; | ||
} | ||
| { | ||
type: "clearPendingLink"; | ||
}; | ||
|
||
export const exitConfirmationReducer: Reducer<State, Action> = ( | ||
state, | ||
action | ||
) => { | ||
switch (action.type) { | ||
case "addExitConfirmationCheck": | ||
return { | ||
...state, | ||
checks: { | ||
...state.checks, | ||
[action.payload.key]: action.payload.check, | ||
}, | ||
}; | ||
case "removeExitConfirmationCheck": | ||
const { [action.payload.key]: _, ...blockers } = state.checks; | ||
return { ...state, checks: blockers }; | ||
case "intercept": | ||
return { ...state, pendingLink: action.payload.link }; | ||
case "clearPendingLink": | ||
return { ...state, pendingLink: undefined }; | ||
default: | ||
return state; | ||
} | ||
}; | ||
|
||
export const ExitConfirmationWrapperContext = createContext<{ | ||
state: State; | ||
dispatch: (action: Action) => void; | ||
}>({ | ||
state: { | ||
checks: {}, | ||
pendingLink: undefined, | ||
}, | ||
dispatch: () => {}, | ||
}); |
57 changes: 57 additions & 0 deletions
57
packages/hub/src/components/ExitConfirmationWrapper/hooks.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,57 @@ | ||
import { use, useCallback, useEffect, useId } from "react"; | ||
|
||
import { ExitConfirmationWrapperContext } from "./context"; | ||
|
||
/* | ||
* Be very careful with this hook! `check` parameter, if set, must have stable identity. | ||
* | ||
* If it's an inline function, it will cause an infinite render loop: | ||
* - any change in the check function will cause all consumers of this hook to re-render | ||
* - this will cause the check function to re-create, causing another re-render. | ||
*/ | ||
export function useExitConfirmation(check: () => boolean = () => true) { | ||
const { dispatch } = use(ExitConfirmationWrapperContext); | ||
|
||
const key = useId(); | ||
|
||
useEffect(() => { | ||
dispatch({ | ||
type: "addExitConfirmationCheck", | ||
payload: { | ||
key, | ||
check, | ||
}, | ||
}); | ||
|
||
const listener = (e: BeforeUnloadEvent) => { | ||
if (check()) { | ||
e.preventDefault(); | ||
} | ||
}; | ||
window.addEventListener("beforeunload", listener); | ||
|
||
return () => { | ||
window.removeEventListener("beforeunload", listener); | ||
dispatch({ | ||
type: "removeExitConfirmationCheck", | ||
payload: { key }, | ||
}); | ||
}; | ||
}, [dispatch, key, check]); | ||
} | ||
|
||
// set `pendingLink`, show a modal | ||
export function useConfirmNavigation() { | ||
const { dispatch } = use(ExitConfirmationWrapperContext); | ||
|
||
return useCallback( | ||
(link: string) => dispatch({ type: "intercept", payload: { link } }), | ||
[dispatch] | ||
); | ||
} | ||
|
||
// used by `Link` component to detect if exit confirmation is active | ||
export function useIsExitConfirmationActive() { | ||
const { checks: blockers } = use(ExitConfirmationWrapperContext).state; | ||
return () => Object.values(blockers).some((check) => check()); | ||
} |
43 changes: 43 additions & 0 deletions
43
packages/hub/src/components/ExitConfirmationWrapper/index.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,43 @@ | ||
import { FC, PropsWithChildren, useReducer } from "react"; | ||
|
||
import { ConfirmNavigationModal } from "./ConfirmNavigationModal"; | ||
import { | ||
exitConfirmationReducer, | ||
ExitConfirmationWrapperContext, | ||
} from "./context"; | ||
|
||
/* | ||
* This component wraps the application and provides the context for exit | ||
* confirmation when some component in the tree below requires it. | ||
* | ||
* There are two types of exits: | ||
* 1. Navigation to another page. | ||
* 2. Closing the tab. | ||
* | ||
* To enable exit confirmation, use the `useExitConfirmation` hook. | ||
* | ||
* It will set up the `beforeunload` event listener and register the check. | ||
* | ||
* On navigation, `<Link>` component will check if there are any active checks, | ||
* and activate the confirmation modal if necessary. | ||
*/ | ||
export const ExitConfirmationWrapper: FC<PropsWithChildren> = ({ | ||
children, | ||
}) => { | ||
const [state, dispatch] = useReducer(exitConfirmationReducer, { | ||
checks: {}, | ||
pendingLink: undefined, | ||
}); | ||
|
||
return ( | ||
<ExitConfirmationWrapperContext.Provider | ||
value={{ | ||
state, | ||
dispatch, | ||
}} | ||
> | ||
<ConfirmNavigationModal /> | ||
{children} | ||
</ExitConfirmationWrapperContext.Provider> | ||
); | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.