-
Notifications
You must be signed in to change notification settings - Fork 337
Expand file tree
/
Copy pathDialogStackProvider.tsx
More file actions
116 lines (94 loc) · 3.76 KB
/
DialogStackProvider.tsx
File metadata and controls
116 lines (94 loc) · 3.76 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
/** @file This file provides the DialogStackProvider component and related functionality. */
import * as React from 'react'
import invariant from 'tiny-invariant'
import type { StoreApi } from '#/utilities/zustand'
import { createStore, useStore } from '#/utilities/zustand'
import { findLastIndex } from '@/util/data/array'
/** Returns only dialog items from the full overlay stack. */
function getDialogsStack(stack: DialogStackItem[]) {
return stack.filter((stackItem) => ['dialog-fullscreen', 'dialog'].includes(stackItem.type))
}
/** DialogStackItem represents an item in the dialog stack. */
export interface DialogStackItem {
readonly id: string
readonly type: 'dialog-fullscreen' | 'dialog' | 'popover'
}
/** DialogStackContextType represents the context for the dialog stack. */
export interface DialogStackContextType {
readonly stack: DialogStackItem[]
readonly dialogsStack: DialogStackItem[]
readonly add: (item: DialogStackItem) => void
readonly slice: (currentId: string) => void
}
const DialogStackContext = React.createContext<StoreApi<DialogStackContextType> | null>(null)
/** DialogStackProvider is a React component that provides the dialog stack context to its children. */
export function DialogStackProvider(props: React.PropsWithChildren) {
const { children } = props
const [store] = React.useState(() =>
createStore<DialogStackContextType>((set) => ({
stack: [],
dialogsStack: [],
add: (item) => {
set((state) => {
const nextStack = [...state.stack, item]
return {
stack: nextStack,
dialogsStack: getDialogsStack(nextStack),
}
})
},
slice: (currentId) => {
set((state) => {
const index = findLastIndex(state.stack, (item) => item.id === currentId)
if (index == null) {
// eslint-disable-next-line no-restricted-properties
console.warn(`
DialogStackProvider: sliceFromStack: currentId ${currentId} is not present in the stack. \
This is no-op but it might be a sign of a bug in the application. \
Usually, this means that the underlaying component was closed manually or the stack was not \
updated properly.
`)
return state
}
const nextStack = [...state.stack.slice(0, index), ...state.stack.slice(index + 1)]
return {
stack: nextStack,
dialogsStack: getDialogsStack(nextStack),
}
})
},
})),
)
return <DialogStackContext.Provider value={store}>{children}</DialogStackContext.Provider>
}
/** DialogStackRegistrar is a React component that registers a dialog in the dialog stack. */
export const DialogStackRegistrar = React.memo(function DialogStackRegistrar(
props: DialogStackItem,
) {
const { id, type } = props
const store = React.useContext(DialogStackContext)
invariant(store, 'DialogStackRegistrar must be used within a DialogStackProvider')
const { add, slice } = useStore(store, (state) => ({ add: state.add, slice: state.slice }), {
areEqual: 'shallow',
})
React.useEffect(() => {
React.startTransition(() => {
add({ id, type })
})
return () => {
React.startTransition(() => {
slice(id)
})
}
}, [add, slice, id, type])
return null
})
/**
* Hook that returns true if the given id is the latest item in the dialog stack.
*/
// eslint-disable-next-line react-refresh/only-export-components
export function useIsLatestDialogStackItem(id: string) {
const store = React.useContext(DialogStackContext)
invariant(store, 'useIsLatestDialogStackItem must be used within a DialogStackProvider')
return useStore(store, (state) => state.stack.at(-1)?.id === id, { unsafeEnableTransition: true })
}