@tanstack/react-query-devtools 4.10.4 → 4.12.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,7 +1,7 @@
1
1
  {
2
2
  "name": "@tanstack/react-query-devtools",
3
- "version": "4.10.4",
4
- "description": "TODO",
3
+ "version": "4.12.0",
4
+ "description": "Developer tools to interact with and visualize the TanStack/react-query cache",
5
5
  "author": "tannerlinsley",
6
6
  "license": "MIT",
7
7
  "repository": "tanstack/query",
@@ -38,9 +38,15 @@
38
38
  "src"
39
39
  ],
40
40
  "devDependencies": {
41
+ "@types/react": "^18.0.14",
42
+ "@types/react-dom": "^18.0.5",
41
43
  "@types/use-sync-external-store": "^0.0.3",
44
+ "react": "^18.2.0",
45
+ "react-17": "npm:react@^17.0.2",
46
+ "react-dom": "^18.2.0",
47
+ "react-dom-17": "npm:react-dom@^17.0.2",
42
48
  "react-error-boundary": "^3.1.4",
43
- "@tanstack/react-query": "4.10.3"
49
+ "@tanstack/react-query": "4.12.0"
44
50
  },
45
51
  "dependencies": {
46
52
  "@tanstack/match-sorter-utils": "^8.1.1",
@@ -50,10 +56,12 @@
50
56
  "peerDependencies": {
51
57
  "react": "^16.8.0 || ^17.0.0 || ^18.0.0",
52
58
  "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0",
53
- "@tanstack/react-query": "4.10.3"
59
+ "@tanstack/react-query": "4.12.0"
54
60
  },
55
61
  "scripts": {
56
62
  "clean": "rm -rf ./build",
57
- "test:eslint": "../../node_modules/.bin/eslint --ext .ts,.tsx ./src"
63
+ "test:eslint": "../../node_modules/.bin/eslint --ext .ts,.tsx ./src",
64
+ "test:jest": "../../node_modules/.bin/jest --config ./jest.config.ts",
65
+ "test:dev": "pnpm run test:jest --watch"
58
66
  }
59
67
  }
@@ -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,80 +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
- direction: 'ltr',
253
- position: 'fixed',
254
- bottom: '0',
255
- right: '0',
256
- zIndex: 99999,
257
- width: '100%',
258
- height: devtoolsHeight ?? 500,
259
- maxHeight: '90%',
260
- boxShadow: '0 0 20px rgba(0,0,0,.3)',
261
- borderTop: `1px solid ${theme.gray}`,
262
- transformOrigin: 'top',
263
- // visibility will be toggled after transitions, but set initial state here
264
- visibility: isOpen ? 'visible' : 'hidden',
265
- ...panelStyle,
266
- ...(isResizing
267
- ? {
268
- transition: `none`,
269
- }
270
- : { transition: `all .2s ease` }),
271
- ...(isResolvedOpen
272
- ? {
273
- opacity: 1,
274
- pointerEvents: 'all',
275
- transform: `translateY(0) scale(1)`,
276
- }
277
- : {
278
- opacity: 0,
279
- pointerEvents: 'none',
280
- transform: `translateY(15px) scale(1.02)`,
281
- }),
282
- }}
324
+ style={style}
283
325
  isOpen={isResolvedOpen}
284
326
  setIsOpen={setIsOpen}
285
- handleDragStart={(e) => handleDragStart(panelRef.current, e)}
327
+ onDragStart={(e) => handleDragStart(panelRef.current, e)}
286
328
  />
287
- {isResolvedOpen ? (
288
- <Button
289
- type="button"
290
- aria-controls="ReactQueryDevtoolsPanel"
291
- aria-haspopup="true"
292
- aria-expanded="true"
293
- {...(otherCloseButtonProps as Record<string, unknown>)}
294
- onClick={(e) => {
295
- setIsOpen(false)
296
- onCloseClick?.(e)
297
- }}
298
- style={{
299
- position: 'fixed',
300
- zIndex: 99999,
301
- margin: '.5em',
302
- bottom: 0,
303
- ...(position === 'top-right'
304
- ? {
305
- right: '0',
306
- }
307
- : position === 'top-left'
308
- ? {
309
- left: '0',
310
- }
311
- : position === 'bottom-right'
312
- ? {
313
- right: '0',
314
- }
315
- : {
316
- left: '0',
317
- }),
318
- ...closeButtonStyle,
319
- }}
320
- >
321
- Close
322
- </Button>
323
- ) : null}
324
329
  </ThemeProvider>
325
330
  {!isResolvedOpen ? (
326
331
  <button
@@ -404,11 +409,17 @@ export const ReactQueryDevtoolsPanel = React.forwardRef<
404
409
  isOpen = true,
405
410
  styleNonce,
406
411
  setIsOpen,
407
- handleDragStart,
408
412
  context,
413
+ onDragStart,
414
+ onPositionChange,
415
+ showCloseButton,
416
+ position,
417
+ closeButtonProps = {},
409
418
  ...panelProps
410
419
  } = props
411
420
 
421
+ const { onClick: onCloseClick, ...otherCloseButtonProps } = closeButtonProps
422
+
412
423
  const queryClient = useQueryClient({ context })
413
424
  const queryCache = queryClient.getQueryCache()
414
425
 
@@ -467,6 +478,11 @@ export const ReactQueryDevtoolsPanel = React.forwardRef<
467
478
  aria-label="React Query Devtools Panel"
468
479
  id="ReactQueryDevtoolsPanel"
469
480
  {...panelProps}
481
+ style={{
482
+ height: defaultPanelSize,
483
+ position: 'relative',
484
+ ...panelProps.style,
485
+ }}
470
486
  >
471
487
  <style
472
488
  nonce={styleNonce}
@@ -494,17 +510,8 @@ export const ReactQueryDevtoolsPanel = React.forwardRef<
494
510
  }}
495
511
  />
496
512
  <div
497
- style={{
498
- position: 'absolute',
499
- left: 0,
500
- top: 0,
501
- width: '100%',
502
- height: '4px',
503
- marginBottom: '-4px',
504
- cursor: 'row-resize',
505
- zIndex: 100000,
506
- }}
507
- onMouseDown={handleDragStart}
513
+ style={getResizeHandleStyle(position)}
514
+ onMouseDown={onDragStart}
508
515
  ></div>
509
516
 
510
517
  {isOpen && (
@@ -553,7 +560,29 @@ export const ReactQueryDevtoolsPanel = React.forwardRef<
553
560
  flexDirection: 'column',
554
561
  }}
555
562
  >
556
- <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>
557
586
  <div
558
587
  style={{
559
588
  display: 'flex',
@@ -705,6 +734,30 @@ export const ReactQueryDevtoolsPanel = React.forwardRef<
705
734
  queryClient={queryClient}
706
735
  />
707
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}
708
761
  </Panel>
709
762
  </ThemeProvider>
710
763
  )
@@ -974,7 +1027,7 @@ const QueryStatusCount = ({ queryCache }: { queryCache: QueryCache }) => {
974
1027
  .length,
975
1028
  )
976
1029
  return (
977
- <QueryKeys style={{ marginBottom: '.5em' }}>
1030
+ <QueryKeys>
978
1031
  <QueryKey
979
1032
  style={{
980
1033
  background: theme.success,