1
1
import type { Dispatch , SetStateAction } from 'react'
2
- import type { OverlayState } from '../../../../shared'
2
+ import { STORAGE_KEY_POSITION , type OverlayState } from '../../../../shared'
3
3
4
4
import { useState , useEffect , useRef , createContext , useContext } from 'react'
5
5
import { Toast } from '../../toast'
@@ -9,7 +9,8 @@ import { useIsDevRendering } from '../../../../utils/dev-indicator/dev-render-in
9
9
import { useDelayedRender } from '../../../hooks/use-delayed-render'
10
10
import { TurbopackInfo } from './dev-tools-info/turbopack-info'
11
11
import { RouteInfo } from './dev-tools-info/route-info'
12
- import { StopIcon } from '../../../icons/stop-icon'
12
+ import GearIcon from '../../../icons/gear-icon'
13
+ import { UserPreferences } from './dev-tools-info/user-preferences'
13
14
14
15
// TODO: add E2E tests to cover different scenarios
15
16
@@ -18,23 +19,20 @@ const INDICATOR_POSITION =
18
19
. __NEXT_DEV_INDICATOR_POSITION as typeof window . __NEXT_DEV_INDICATOR_POSITION ) ||
19
20
'bottom-left'
20
21
21
- type DevToolsIndicatorPosition = typeof INDICATOR_POSITION
22
+ export type DevToolsIndicatorPosition = typeof INDICATOR_POSITION
22
23
23
24
export function DevToolsIndicator ( {
24
25
state,
25
26
errorCount,
26
27
isBuildError,
27
28
setIsErrorOverlayOpen,
28
- position = INDICATOR_POSITION ,
29
29
} : {
30
30
state : OverlayState
31
31
errorCount : number
32
32
isBuildError : boolean
33
33
setIsErrorOverlayOpen : (
34
34
isErrorOverlayOpen : boolean | ( ( prev : boolean ) => boolean )
35
35
) => void
36
- // Technically this prop isn't needed, but useful for testing.
37
- position ?: DevToolsIndicatorPosition
38
36
} ) {
39
37
const [ isDevToolsIndicatorVisible , setIsDevToolsIndicatorVisible ] =
40
38
useState ( true )
@@ -53,7 +51,6 @@ export function DevToolsIndicator({
53
51
} }
54
52
setIsErrorOverlayOpen = { setIsErrorOverlayOpen }
55
53
isTurbopack = { ! ! process . env . TURBOPACK }
56
- position = { position }
57
54
disabled = { state . disableDevIndicator || ! isDevToolsIndicatorVisible }
58
55
isBuildError = { isBuildError }
59
56
/>
@@ -73,13 +70,25 @@ interface C {
73
70
74
71
const Context = createContext ( { } as C )
75
72
73
+ function getInitialPosition ( ) {
74
+ if (
75
+ typeof localStorage !== 'undefined' &&
76
+ localStorage . getItem ( STORAGE_KEY_POSITION )
77
+ ) {
78
+ return localStorage . getItem (
79
+ STORAGE_KEY_POSITION
80
+ ) as DevToolsIndicatorPosition
81
+ }
82
+
83
+ return INDICATOR_POSITION
84
+ }
85
+
76
86
function DevToolsPopover ( {
77
87
routerType,
78
88
disabled,
79
89
issueCount,
80
90
isStaticRoute,
81
91
isTurbopack,
82
- position,
83
92
isBuildError,
84
93
hide,
85
94
setIsErrorOverlayOpen,
@@ -90,7 +99,6 @@ function DevToolsPopover({
90
99
isStaticRoute : boolean
91
100
semver : string | undefined
92
101
isTurbopack : boolean
93
- position : DevToolsIndicatorPosition
94
102
isBuildError : boolean
95
103
hide : ( ) => void
96
104
setIsErrorOverlayOpen : (
@@ -103,10 +111,14 @@ function DevToolsPopover({
103
111
const triggerTurbopackRef = useRef < HTMLButtonElement | null > ( null )
104
112
const routeInfoRef = useRef < HTMLElement > ( null )
105
113
const triggerRouteInfoRef = useRef < HTMLButtonElement | null > ( null )
114
+ const preferencesRef = useRef < HTMLElement > ( null )
115
+ const triggerPreferencesRef = useRef < HTMLButtonElement | null > ( null )
106
116
const [ isMenuOpen , setIsMenuOpen ] = useState ( false )
107
117
const [ isTurbopackInfoOpen , setIsTurbopackInfoOpen ] = useState ( false )
108
118
const [ isRouteInfoOpen , setIsRouteInfoOpen ] = useState ( false )
119
+ const [ isPreferencesOpen , setIsPreferencesOpen ] = useState ( false )
109
120
const [ selectedIndex , setSelectedIndex ] = useState ( - 1 )
121
+ const [ position , setPosition ] = useState ( getInitialPosition ( ) )
110
122
111
123
// This hook lets us do an exit animation before unmounting the component
112
124
const { mounted : menuMounted , rendered : menuRendered } = useDelayedRender (
@@ -128,6 +140,11 @@ function DevToolsPopover({
128
140
enterDelay : 0 ,
129
141
exitDelay : ANIMATE_OUT_DURATION_MS ,
130
142
} )
143
+ const { mounted : preferencesMounted , rendered : preferencesRendered } =
144
+ useDelayedRender ( isPreferencesOpen , {
145
+ enterDelay : 0 ,
146
+ exitDelay : ANIMATE_OUT_DURATION_MS ,
147
+ } )
131
148
132
149
// Features to make the menu accessible
133
150
useFocusTrap ( menuRef , triggerRef , isMenuOpen )
@@ -146,7 +163,13 @@ function DevToolsPopover({
146
163
isRouteInfoOpen ,
147
164
closeRouteInfo
148
165
)
149
-
166
+ useFocusTrap ( preferencesRef , triggerPreferencesRef , isPreferencesOpen )
167
+ useClickOutside (
168
+ preferencesRef ,
169
+ triggerPreferencesRef ,
170
+ isPreferencesOpen ,
171
+ closePreferences
172
+ )
150
173
function select ( index : number | 'first' | 'last' ) {
151
174
if ( index === 'first' ) {
152
175
const all = menuRef . current ?. querySelectorAll ( '[role="menuitem"]' )
@@ -254,6 +277,15 @@ function DevToolsPopover({
254
277
setIsRouteInfoOpen ( false )
255
278
}
256
279
280
+ function closePreferences ( ) {
281
+ setIsPreferencesOpen ( false )
282
+ }
283
+
284
+ function handleHideDevtools ( ) {
285
+ closePreferences ( )
286
+ hide ( )
287
+ }
288
+
257
289
const [ vertical , horizontal ] = position . split ( '-' , 2 )
258
290
259
291
return (
@@ -316,6 +348,21 @@ function DevToolsPopover({
316
348
/>
317
349
) }
318
350
351
+ { preferencesMounted && (
352
+ < UserPreferences
353
+ ref = { preferencesRef }
354
+ isOpen = { isPreferencesOpen }
355
+ hide = { handleHideDevtools }
356
+ setPosition = { setPosition }
357
+ position = { position }
358
+ style = { {
359
+ [ vertical ] : 'calc(100% + 8px)' ,
360
+ [ horizontal ] : 0 ,
361
+ } }
362
+ data-rendered = { preferencesRendered }
363
+ />
364
+ ) }
365
+
319
366
{ menuMounted && (
320
367
< div
321
368
ref = { menuRef }
@@ -381,11 +428,10 @@ function DevToolsPopover({
381
428
382
429
< div className = "dev-tools-indicator-footer" >
383
430
< MenuItem
384
- data-hide-dev-tools
385
- title = "Hide Dev Tools for the current server session or a day."
386
- label = "Hide for Dev Session"
387
- value = { < StopIcon /> }
388
- onClick = { hide }
431
+ data-preferences
432
+ label = "Preferences"
433
+ value = { < GearIcon /> }
434
+ onClick = { ( ) => setIsPreferencesOpen ( true ) }
389
435
index = { isTurbopack ? 2 : 3 }
390
436
/>
391
437
</ div >
0 commit comments