@tanstack/react-query-devtools 4.10.3 → 4.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tanstack/react-query-devtools",
3
- "version": "4.10.3",
3
+ "version": "4.11.0",
4
4
  "description": "TODO",
5
5
  "author": "tannerlinsley",
6
6
  "license": "MIT",
@@ -764,4 +764,103 @@ describe('ReactQueryDevtools', () => {
764
764
  consoleErrorMock.mockRestore()
765
765
  })
766
766
  })
767
+
768
+ it('should render a menu to select panel position', async () => {
769
+ const { queryClient } = createQueryClient()
770
+
771
+ function Page() {
772
+ const { data = 'default' } = useQuery(['check'], async () => 'test')
773
+
774
+ return (
775
+ <div>
776
+ <h1>{data}</h1>
777
+ </div>
778
+ )
779
+ }
780
+
781
+ renderWithClient(queryClient, <Page />, {
782
+ initialIsOpen: true,
783
+ })
784
+
785
+ const positionSelect = (await screen.findByLabelText(
786
+ 'Panel position',
787
+ )) as HTMLSelectElement
788
+
789
+ expect(positionSelect.value).toBe('bottom')
790
+ })
791
+
792
+ it(`should render the panel to the left if panelPosition is set to 'left'`, async () => {
793
+ const { queryClient } = createQueryClient()
794
+
795
+ function Page() {
796
+ const { data = 'default' } = useQuery(['check'], async () => 'test')
797
+
798
+ return (
799
+ <div>
800
+ <h1>{data}</h1>
801
+ </div>
802
+ )
803
+ }
804
+
805
+ renderWithClient(queryClient, <Page />, {
806
+ initialIsOpen: true,
807
+ panelPosition: 'left',
808
+ })
809
+
810
+ const positionSelect = (await screen.findByLabelText(
811
+ 'Panel position',
812
+ )) as HTMLSelectElement
813
+
814
+ expect(positionSelect.value).toBe('left')
815
+
816
+ const panel = (await screen.getByLabelText(
817
+ 'React Query Devtools Panel',
818
+ )) as HTMLDivElement
819
+
820
+ expect(panel.style.left).toBe('0px')
821
+ expect(panel.style.width).toBe('500px')
822
+ expect(panel.style.height).toBe('100vh')
823
+ })
824
+
825
+ it('should change the panel position if user select different option from the menu', async () => {
826
+ const { queryClient } = createQueryClient()
827
+
828
+ function Page() {
829
+ const { data = 'default' } = useQuery(['check'], async () => 'test')
830
+
831
+ return (
832
+ <div>
833
+ <h1>{data}</h1>
834
+ </div>
835
+ )
836
+ }
837
+
838
+ renderWithClient(queryClient, <Page />, {
839
+ initialIsOpen: true,
840
+ })
841
+
842
+ const positionSelect = (await screen.findByLabelText(
843
+ 'Panel position',
844
+ )) as HTMLSelectElement
845
+
846
+ expect(positionSelect.value).toBe('bottom')
847
+
848
+ const panel = (await screen.getByLabelText(
849
+ 'React Query Devtools Panel',
850
+ )) as HTMLDivElement
851
+
852
+ expect(panel.style.bottom).toBe('0px')
853
+ expect(panel.style.height).toBe('500px')
854
+ expect(panel.style.width).toBe('100%')
855
+
856
+ await act(async () => {
857
+ fireEvent.change(positionSelect, { target: { value: 'right' } })
858
+ })
859
+
860
+ expect(positionSelect.value).toBe('right')
861
+
862
+ expect(panel.style.right).toBe('0px')
863
+ expect(panel.style.width).toBe('500px')
864
+ expect(panel.style.height).toBe('100vh')
865
+ })
767
866
  })
package/src/devtools.tsx CHANGED
@@ -13,9 +13,17 @@ import {
13
13
  } from '@tanstack/react-query'
14
14
  import { rankItem } from '@tanstack/match-sorter-utils'
15
15
  import useLocalStorage from './useLocalStorage'
16
- import { sortFns, useIsMounted } from './utils'
17
- import ScreenReader from './screenreader'
18
-
16
+ import {
17
+ isVerticalSide,
18
+ sortFns,
19
+ useIsMounted,
20
+ getSidePanelStyle,
21
+ minPanelSize,
22
+ getResizeHandleStyle,
23
+ getSidedProp,
24
+ defaultPanelSize,
25
+ } from './utils'
26
+ import type { Corner, Side } from './utils'
19
27
  import {
20
28
  Panel,
21
29
  QueryKeys,
@@ -26,6 +34,7 @@ import {
26
34
  Select,
27
35
  ActiveQueryPanel,
28
36
  } from './styledComponents'
37
+ import ScreenReader from './screenreader'
29
38
  import { ThemeProvider, defaultTheme as theme } from './theme'
30
39
  import { getQueryStatusLabel, getQueryStatusColor } from './utils'
31
40
  import Explorer from './Explorer'
@@ -39,29 +48,25 @@ export interface DevtoolsOptions extends ContextOptions {
39
48
  /**
40
49
  * Use this to add props to the panel. For example, you can add className, style (merge and override default style), etc.
41
50
  */
42
- panelProps?: React.DetailedHTMLProps<
43
- React.HTMLAttributes<HTMLDivElement>,
44
- HTMLDivElement
45
- >
51
+ panelProps?: React.ComponentPropsWithoutRef<'div'>
46
52
  /**
47
53
  * Use this to add props to the close button. For example, you can add className, style (merge and override default style), onClick (extend default handler), etc.
48
54
  */
49
- closeButtonProps?: React.DetailedHTMLProps<
50
- React.ButtonHTMLAttributes<HTMLButtonElement>,
51
- HTMLButtonElement
52
- >
55
+ closeButtonProps?: React.ComponentPropsWithoutRef<'button'>
53
56
  /**
54
57
  * Use this to add props to the toggle button. For example, you can add className, style (merge and override default style), onClick (extend default handler), etc.
55
58
  */
56
- toggleButtonProps?: React.DetailedHTMLProps<
57
- React.ButtonHTMLAttributes<HTMLButtonElement>,
58
- HTMLButtonElement
59
- >
59
+ toggleButtonProps?: React.ComponentPropsWithoutRef<'button'>
60
60
  /**
61
61
  * The position of the React Query logo to open and close the devtools panel.
62
62
  * Defaults to 'bottom-left'.
63
63
  */
64
- position?: 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right'
64
+ position?: Corner
65
+ /**
66
+ * The position of the React Query devtools panel.
67
+ * Defaults to 'bottom'.
68
+ */
69
+ panelPosition?: Side
65
70
  /**
66
71
  * Use this to render the devtools inside a different type of container element for a11y purposes.
67
72
  * Any string which corresponds to a valid intrinsic JSX element is allowed.
@@ -98,7 +103,24 @@ interface DevtoolsPanelOptions extends ContextOptions {
98
103
  /**
99
104
  * Handles the opening and closing the devtools panel
100
105
  */
101
- handleDragStart: (e: React.MouseEvent<HTMLDivElement, MouseEvent>) => void
106
+ onDragStart: (e: React.MouseEvent<HTMLDivElement, MouseEvent>) => void
107
+ /**
108
+ * The position of the React Query devtools panel.
109
+ * Defaults to 'bottom'.
110
+ */
111
+ position?: Side
112
+ /**
113
+ * Handles the panel position select change
114
+ */
115
+ onPositionChange?: (side: Side) => void
116
+ /**
117
+ * Show a close button inside the panel
118
+ */
119
+ showCloseButton?: boolean
120
+ /**
121
+ * Use this to add props to the close button. For example, you can add className, style (merge and override default style), onClick (extend default handler), etc.
122
+ */
123
+ closeButtonProps?: React.ComponentPropsWithoutRef<'button'>
102
124
  }
103
125
 
104
126
  export function ReactQueryDevtools({
@@ -110,6 +132,7 @@ export function ReactQueryDevtools({
110
132
  containerElement: Container = 'aside',
111
133
  context,
112
134
  styleNonce,
135
+ panelPosition: initialPanelPosition = 'bottom',
113
136
  }: DevtoolsOptions): React.ReactElement | null {
114
137
  const rootRef = React.useRef<HTMLDivElement>(null)
115
138
  const panelRef = React.useRef<HTMLDivElement>(null)
@@ -117,10 +140,20 @@ export function ReactQueryDevtools({
117
140
  'reactQueryDevtoolsOpen',
118
141
  initialIsOpen,
119
142
  )
120
- const [devtoolsHeight, setDevtoolsHeight] = useLocalStorage<number | null>(
143
+ const [devtoolsHeight, setDevtoolsHeight] = useLocalStorage<number>(
121
144
  'reactQueryDevtoolsHeight',
122
- null,
145
+ defaultPanelSize,
146
+ )
147
+ const [devtoolsWidth, setDevtoolsWidth] = useLocalStorage<number>(
148
+ 'reactQueryDevtoolsWidth',
149
+ defaultPanelSize,
150
+ )
151
+
152
+ const [panelPosition = 'bottom', setPanelPosition] = useLocalStorage<Side>(
153
+ 'reactQueryDevtoolsPanelPosition',
154
+ initialPanelPosition,
123
155
  )
156
+
124
157
  const [isResolvedOpen, setIsResolvedOpen] = React.useState(false)
125
158
  const [isResizing, setIsResizing] = React.useState(false)
126
159
  const isMounted = useIsMounted()
@@ -129,22 +162,39 @@ export function ReactQueryDevtools({
129
162
  panelElement: HTMLDivElement | null,
130
163
  startEvent: React.MouseEvent<HTMLDivElement, MouseEvent>,
131
164
  ) => {
165
+ if (!panelElement) return
132
166
  if (startEvent.button !== 0) return // Only allow left click for drag
133
-
167
+ const isVertical = isVerticalSide(panelPosition)
134
168
  setIsResizing(true)
135
169
 
136
- const dragInfo = {
137
- originalHeight: panelElement?.getBoundingClientRect().height ?? 0,
138
- pageY: startEvent.pageY,
139
- }
170
+ const { height, width } = panelElement.getBoundingClientRect()
171
+ const startX = startEvent.clientX
172
+ const startY = startEvent.clientY
173
+ let newSize = 0
140
174
 
141
175
  const run = (moveEvent: MouseEvent) => {
142
- const delta = dragInfo.pageY - moveEvent.pageY
143
- const newHeight = dragInfo.originalHeight + delta
144
-
145
- setDevtoolsHeight(newHeight)
176
+ // prevent mouse selecting stuff with mouse drag
177
+ moveEvent.preventDefault()
178
+
179
+ // calculate the correct size based on mouse position and current panel position
180
+ // hint: it is different formula for the opposite sides
181
+ if (isVertical) {
182
+ newSize =
183
+ width +
184
+ (panelPosition === 'right'
185
+ ? startX - moveEvent.clientX
186
+ : moveEvent.clientX - startX)
187
+ setDevtoolsWidth(newSize)
188
+ } else {
189
+ newSize =
190
+ height +
191
+ (panelPosition === 'bottom'
192
+ ? startY - moveEvent.clientY
193
+ : moveEvent.clientY - startY)
194
+ setDevtoolsHeight(newSize)
195
+ }
146
196
 
147
- if (newHeight < 70) {
197
+ if (newSize < minPanelSize) {
148
198
  setIsOpen(false)
149
199
  } else {
150
200
  setIsOpen(true)
@@ -152,13 +202,16 @@ export function ReactQueryDevtools({
152
202
  }
153
203
 
154
204
  const unsub = () => {
155
- setIsResizing(false)
156
- document.removeEventListener('mousemove', run)
157
- document.removeEventListener('mouseUp', unsub)
205
+ if (isResizing) {
206
+ setIsResizing(false)
207
+ }
208
+
209
+ document.removeEventListener('mousemove', run, false)
210
+ document.removeEventListener('mouseUp', unsub, false)
158
211
  }
159
212
 
160
- document.addEventListener('mousemove', run)
161
- document.addEventListener('mouseup', unsub)
213
+ document.addEventListener('mousemove', run, false)
214
+ document.addEventListener('mouseup', unsub, false)
162
215
  }
163
216
 
164
217
  React.useEffect(() => {
@@ -195,12 +248,23 @@ export function ReactQueryDevtools({
195
248
  React.useEffect(() => {
196
249
  if (isResolvedOpen) {
197
250
  const root = rootRef.current
198
- const previousValue = root?.parentElement?.style.paddingBottom
251
+ const styleProp = getSidedProp('padding', panelPosition)
252
+ const isVertical = isVerticalSide(panelPosition)
253
+ const previousValue = root?.parentElement?.style[styleProp]
199
254
 
200
255
  const run = () => {
201
- const containerHeight = panelRef.current?.getBoundingClientRect().height
202
256
  if (root?.parentElement) {
203
- root.parentElement.style.paddingBottom = `${containerHeight}px`
257
+ // reset the padding
258
+ root.parentElement.style.padding = '0px'
259
+ root.parentElement.style.paddingTop = '0px'
260
+ root.parentElement.style.paddingBottom = '0px'
261
+ root.parentElement.style.paddingLeft = '0px'
262
+ root.parentElement.style.paddingRight = '0px'
263
+ // set the new padding based on the new panel position
264
+
265
+ root.parentElement.style[styleProp] = `${
266
+ isVertical ? devtoolsWidth : devtoolsHeight
267
+ }px`
204
268
  }
205
269
  }
206
270
 
@@ -212,27 +276,32 @@ export function ReactQueryDevtools({
212
276
  return () => {
213
277
  window.removeEventListener('resize', run)
214
278
  if (root?.parentElement && typeof previousValue === 'string') {
215
- root.parentElement.style.paddingBottom = previousValue
279
+ root.parentElement.style[styleProp] = previousValue
216
280
  }
217
281
  }
218
282
  }
219
283
  }
220
- }, [isResolvedOpen])
284
+ }, [isResolvedOpen, panelPosition, devtoolsHeight, devtoolsWidth])
221
285
 
222
286
  const { style: panelStyle = {}, ...otherPanelProps } = panelProps
223
287
 
224
- const {
225
- style: closeButtonStyle = {},
226
- onClick: onCloseClick,
227
- ...otherCloseButtonProps
228
- } = closeButtonProps
229
-
230
288
  const {
231
289
  style: toggleButtonStyle = {},
232
290
  onClick: onToggleClick,
233
291
  ...otherToggleButtonProps
234
292
  } = toggleButtonProps
235
293
 
294
+ // get computed style based on panel position
295
+ const style = getSidePanelStyle({
296
+ position: panelPosition,
297
+ devtoolsTheme: theme,
298
+ isOpen: isResolvedOpen,
299
+ height: devtoolsHeight,
300
+ width: devtoolsWidth,
301
+ isResizing,
302
+ panelStyle,
303
+ })
304
+
236
305
  // Do not render on the server
237
306
  if (!isMounted()) return null
238
307
 
@@ -247,79 +316,16 @@ export function ReactQueryDevtools({
247
316
  ref={panelRef as any}
248
317
  context={context}
249
318
  styleNonce={styleNonce}
319
+ position={panelPosition}
320
+ onPositionChange={setPanelPosition}
321
+ showCloseButton
322
+ closeButtonProps={closeButtonProps}
250
323
  {...otherPanelProps}
251
- style={{
252
- position: 'fixed',
253
- bottom: '0',
254
- right: '0',
255
- zIndex: 99999,
256
- width: '100%',
257
- height: devtoolsHeight ?? 500,
258
- maxHeight: '90%',
259
- boxShadow: '0 0 20px rgba(0,0,0,.3)',
260
- borderTop: `1px solid ${theme.gray}`,
261
- transformOrigin: 'top',
262
- // visibility will be toggled after transitions, but set initial state here
263
- visibility: isOpen ? 'visible' : 'hidden',
264
- ...panelStyle,
265
- ...(isResizing
266
- ? {
267
- transition: `none`,
268
- }
269
- : { transition: `all .2s ease` }),
270
- ...(isResolvedOpen
271
- ? {
272
- opacity: 1,
273
- pointerEvents: 'all',
274
- transform: `translateY(0) scale(1)`,
275
- }
276
- : {
277
- opacity: 0,
278
- pointerEvents: 'none',
279
- transform: `translateY(15px) scale(1.02)`,
280
- }),
281
- }}
324
+ style={style}
282
325
  isOpen={isResolvedOpen}
283
326
  setIsOpen={setIsOpen}
284
- handleDragStart={(e) => handleDragStart(panelRef.current, e)}
327
+ onDragStart={(e) => handleDragStart(panelRef.current, e)}
285
328
  />
286
- {isResolvedOpen ? (
287
- <Button
288
- type="button"
289
- aria-controls="ReactQueryDevtoolsPanel"
290
- aria-haspopup="true"
291
- aria-expanded="true"
292
- {...(otherCloseButtonProps as Record<string, unknown>)}
293
- onClick={(e) => {
294
- setIsOpen(false)
295
- onCloseClick?.(e)
296
- }}
297
- style={{
298
- position: 'fixed',
299
- zIndex: 99999,
300
- margin: '.5em',
301
- bottom: 0,
302
- ...(position === 'top-right'
303
- ? {
304
- right: '0',
305
- }
306
- : position === 'top-left'
307
- ? {
308
- left: '0',
309
- }
310
- : position === 'bottom-right'
311
- ? {
312
- right: '0',
313
- }
314
- : {
315
- left: '0',
316
- }),
317
- ...closeButtonStyle,
318
- }}
319
- >
320
- Close
321
- </Button>
322
- ) : null}
323
329
  </ThemeProvider>
324
330
  {!isResolvedOpen ? (
325
331
  <button
@@ -403,11 +409,17 @@ export const ReactQueryDevtoolsPanel = React.forwardRef<
403
409
  isOpen = true,
404
410
  styleNonce,
405
411
  setIsOpen,
406
- handleDragStart,
407
412
  context,
413
+ onDragStart,
414
+ onPositionChange,
415
+ showCloseButton,
416
+ position,
417
+ closeButtonProps = {},
408
418
  ...panelProps
409
419
  } = props
410
420
 
421
+ const { onClick: onCloseClick, ...otherCloseButtonProps } = closeButtonProps
422
+
411
423
  const queryClient = useQueryClient({ context })
412
424
  const queryCache = queryClient.getQueryCache()
413
425
 
@@ -466,6 +478,11 @@ export const ReactQueryDevtoolsPanel = React.forwardRef<
466
478
  aria-label="React Query Devtools Panel"
467
479
  id="ReactQueryDevtoolsPanel"
468
480
  {...panelProps}
481
+ style={{
482
+ height: defaultPanelSize,
483
+ position: 'relative',
484
+ ...panelProps.style,
485
+ }}
469
486
  >
470
487
  <style
471
488
  nonce={styleNonce}
@@ -493,17 +510,8 @@ export const ReactQueryDevtoolsPanel = React.forwardRef<
493
510
  }}
494
511
  />
495
512
  <div
496
- style={{
497
- position: 'absolute',
498
- left: 0,
499
- top: 0,
500
- width: '100%',
501
- height: '4px',
502
- marginBottom: '-4px',
503
- cursor: 'row-resize',
504
- zIndex: 100000,
505
- }}
506
- onMouseDown={handleDragStart}
513
+ style={getResizeHandleStyle(position)}
514
+ onMouseDown={onDragStart}
507
515
  ></div>
508
516
 
509
517
  {isOpen && (
@@ -552,7 +560,29 @@ export const ReactQueryDevtoolsPanel = React.forwardRef<
552
560
  flexDirection: 'column',
553
561
  }}
554
562
  >
555
- <QueryStatusCount queryCache={queryCache} />
563
+ <div
564
+ style={{
565
+ display: 'flex',
566
+ justifyContent: 'space-between',
567
+ alignItems: 'center',
568
+ marginBottom: '.5em',
569
+ }}
570
+ >
571
+ <QueryStatusCount queryCache={queryCache} />
572
+ {position && onPositionChange ? (
573
+ <Select
574
+ aria-label="Panel position"
575
+ value={position}
576
+ style={{ marginInlineStart: '.5em' }}
577
+ onChange={(e) => onPositionChange(e.target.value as Side)}
578
+ >
579
+ <option value="left">Left</option>
580
+ <option value="right">Right</option>
581
+ <option value="top">Top</option>
582
+ <option value="bottom">Bottom</option>
583
+ </Select>
584
+ ) : null}
585
+ </div>
556
586
  <div
557
587
  style={{
558
588
  display: 'flex',
@@ -704,6 +734,30 @@ export const ReactQueryDevtoolsPanel = React.forwardRef<
704
734
  queryClient={queryClient}
705
735
  />
706
736
  ) : null}
737
+
738
+ {showCloseButton ? (
739
+ <Button
740
+ type="button"
741
+ aria-controls="ReactQueryDevtoolsPanel"
742
+ aria-haspopup="true"
743
+ aria-expanded="true"
744
+ {...(otherCloseButtonProps as Record<string, unknown>)}
745
+ style={{
746
+ position: 'absolute',
747
+ zIndex: 99999,
748
+ margin: '.5em',
749
+ bottom: 0,
750
+ left: 0,
751
+ ...otherCloseButtonProps.style,
752
+ }}
753
+ onClick={(e) => {
754
+ setIsOpen(false)
755
+ onCloseClick?.(e)
756
+ }}
757
+ >
758
+ Close
759
+ </Button>
760
+ ) : null}
707
761
  </Panel>
708
762
  </ThemeProvider>
709
763
  )
@@ -973,7 +1027,7 @@ const QueryStatusCount = ({ queryCache }: { queryCache: QueryCache }) => {
973
1027
  .length,
974
1028
  )
975
1029
  return (
976
- <QueryKeys style={{ marginBottom: '.5em' }}>
1030
+ <QueryKeys>
977
1031
  <QueryKey
978
1032
  style={{
979
1033
  background: theme.success,