Skip to content

Commit d524864

Browse files
akshat-OwOAlemTuzlakautofix-ci[bot]
authored
Custom Trigger Component for devtools (#228)
* feat: custom trigger component - changes in config to accept an optional custom trigger component - replaces default trigger and allows for rendering a custom trigger * docs: add docstring for triggerComponent * fix: remove export from unused exported type * chore: changeset * feat: update implementation * chore: update desc * ci: apply automated fixes --------- Co-authored-by: Alem Tuzlak <t.zlak@hotmail.com> Co-authored-by: Alem Tuzlak <t.zlak97@gmail.com> Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
1 parent 24a3a59 commit d524864

File tree

9 files changed

+143
-40
lines changed

9 files changed

+143
-40
lines changed

.changeset/rare-moles-rest.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
---
2+
'@tanstack/react-devtools': minor
3+
'@tanstack/devtools': minor
4+
---
5+
6+
added optional trigger component in config
7+
8+
removed trigger image setting completely

examples/solid/basic/src/index.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,9 @@ function App() {
1919
<Router />
2020

2121
<TanStackDevtools
22+
config={{
23+
customTrigger: () => <h1>hello world</h1>,
24+
}}
2225
plugins={[
2326
{
2427
name: 'TanStack Query',

packages/devtools/src/components/trigger.tsx

Lines changed: 22 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,37 +1,49 @@
1-
import { Show, createMemo } from 'solid-js'
1+
import { Show, createEffect, createMemo, createSignal } from 'solid-js'
22
import clsx from 'clsx'
33
import { useDevtoolsSettings } from '../context/use-devtools-context'
44
import { useStyles } from '../styles/use-styles'
55
import TanStackLogo from './tanstack-logo.png'
66
import type { Accessor } from 'solid-js'
77

8-
export const Trigger = ({
9-
isOpen,
10-
setIsOpen,
11-
image = TanStackLogo,
12-
}: {
8+
export const Trigger = (props: {
139
isOpen: Accessor<boolean>
1410
setIsOpen: (isOpen: boolean) => void
15-
image: string
1611
}) => {
1712
const { settings } = useDevtoolsSettings()
13+
const [containerRef, setContainerRef] = createSignal<HTMLElement>()
1814
const styles = useStyles()
1915
const buttonStyle = createMemo(() => {
2016
return clsx(
2117
styles().mainCloseBtn,
2218
styles().mainCloseBtnPosition(settings().position),
23-
styles().mainCloseBtnAnimation(isOpen(), settings().hideUntilHover),
19+
styles().mainCloseBtnAnimation(props.isOpen(), settings().hideUntilHover),
2420
)
2521
})
22+
23+
createEffect(() => {
24+
const triggerComponent = settings().customTrigger
25+
const el = containerRef()
26+
if (triggerComponent && el) {
27+
triggerComponent(el, {
28+
theme: settings().theme,
29+
})
30+
}
31+
})
32+
2633
return (
2734
<Show when={!settings().triggerHidden}>
2835
<button
2936
type="button"
3037
aria-label="Open TanStack Devtools"
3138
class={buttonStyle()}
32-
onClick={() => setIsOpen(!isOpen())}
39+
onClick={() => props.setIsOpen(!props.isOpen())}
3340
>
34-
<img src={image || TanStackLogo} alt="TanStack Devtools" />
41+
<Show
42+
when={settings().customTrigger}
43+
fallback={<img src={TanStackLogo} alt="TanStack Devtools" />}
44+
>
45+
<div ref={setContainerRef} />
46+
</Show>
3547
</button>
3648
</Show>
3749
)

packages/devtools/src/context/devtools-store.ts

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,10 @@ type TriggerPosition =
1818
| 'middle-left'
1919
| 'middle-right'
2020

21+
type TriggerProps = {
22+
theme: 'light' | 'dark'
23+
}
24+
2125
export type DevtoolsStore = {
2226
settings: {
2327
/**
@@ -61,15 +65,17 @@ export type DevtoolsStore = {
6165
* @default "dark"
6266
*/
6367
theme: 'light' | 'dark'
64-
/**
65-
* The image used for the dev tools trigger
66-
* @default TanStackLogo
67-
*/
68-
triggerImage: string
68+
6969
/**
7070
* Whether the trigger should be completely hidden or not (you can still open with the hotkey)
7171
*/
7272
triggerHidden?: boolean
73+
/**
74+
* An optional custom function to render the dev tools trigger component.
75+
* If provided, it replaces the default trigger button.
76+
* @default undefined
77+
*/
78+
customTrigger?: (el: HTMLElement, props: TriggerProps) => void
7379
}
7480
state: {
7581
activeTab: TabName
@@ -95,8 +101,8 @@ export const initialState: DevtoolsStore = {
95101
window.matchMedia('(prefers-color-scheme: dark)').matches
96102
? 'dark'
97103
: 'light',
98-
triggerImage: '',
99104
triggerHidden: false,
105+
customTrigger: undefined,
100106
},
101107
state: {
102108
activeTab: 'plugins',

packages/devtools/src/devtools.tsx

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -175,11 +175,7 @@ export default function DevTools() {
175175
: true
176176
}
177177
>
178-
<Trigger
179-
isOpen={isOpen}
180-
setIsOpen={toggleOpen}
181-
image={settings().triggerImage}
182-
/>
178+
<Trigger isOpen={isOpen} setIsOpen={toggleOpen} />
183179
<MainPanel isResizing={isResizing} isOpen={isOpen}>
184180
<ContentPanel
185181
ref={(ref) => (panelRef = ref)}

packages/devtools/src/tabs/settings-tab.tsx

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -79,13 +79,7 @@ export const SettingsTab = () => {
7979
}
8080
checked={settings().triggerHidden}
8181
/>
82-
<Input
83-
label="Trigger Image"
84-
description="Specify the URL of the image to use for the trigger"
85-
value={settings().triggerImage}
86-
placeholder="Default TanStack Logo"
87-
onChange={(value) => setSettings({ triggerImage: value })}
88-
/>
82+
8983
<Select
9084
label="Theme"
9185
description="Choose the theme for the devtools panel"

packages/react-devtools/src/devtools.tsx

Lines changed: 64 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,14 @@ type PluginRender =
1212
| JSX.Element
1313
| ((el: HTMLElement, theme: 'dark' | 'light') => JSX.Element)
1414

15+
type TriggerProps = {
16+
theme: 'dark' | 'light'
17+
}
18+
19+
type TriggerRender =
20+
| JSX.Element
21+
| ((el: HTMLElement, props: TriggerProps) => JSX.Element)
22+
1523
export type TanStackDevtoolsReactPlugin = Omit<
1624
TanStackDevtoolsPlugin,
1725
'render' | 'name'
@@ -57,6 +65,24 @@ export type TanStackDevtoolsReactPlugin = Omit<
5765
name: string | PluginRender
5866
}
5967

68+
type TanStackDevtoolsReactConfig = Omit<
69+
Partial<TanStackDevtoolsConfig>,
70+
'customTrigger'
71+
> & {
72+
/**
73+
* Optional custom trigger component for the devtools.
74+
* It can be a React element or a function that renders one.
75+
*
76+
* Example:
77+
* ```jsx
78+
* {
79+
* customTrigger: <CustomTriggerComponent />,
80+
* }
81+
* ```
82+
*/
83+
customTrigger?: TriggerRender
84+
}
85+
6086
export interface TanStackDevtoolsReactInit {
6187
/**
6288
* Array of plugins to be used in the devtools.
@@ -81,7 +107,7 @@ export interface TanStackDevtoolsReactInit {
81107
* initial state of the devtools when it is started for the first time. Afterwards,
82108
* the settings are persisted in local storage and changed through the settings panel.
83109
*/
84-
config?: Partial<TanStackDevtoolsConfig>
110+
config?: TanStackDevtoolsReactConfig
85111
/**
86112
* Configuration for the TanStack Devtools client event bus.
87113
*/
@@ -105,6 +131,17 @@ const convertRender = (
105131
}))
106132
}
107133

134+
const convertTrigger = (
135+
Component: TriggerRender,
136+
setComponent: React.Dispatch<React.SetStateAction<JSX.Element | null>>,
137+
e: HTMLElement,
138+
props: TriggerProps,
139+
) => {
140+
const element =
141+
typeof Component === 'function' ? Component(e, props) : Component
142+
setComponent(element)
143+
}
144+
108145
export const TanStackDevtools = ({
109146
plugins,
110147
config,
@@ -118,13 +155,19 @@ export const TanStackDevtools = ({
118155
const [titleContainers, setTitleContainers] = useState<
119156
Record<string, HTMLElement>
120157
>({})
158+
const [triggerContainer, setTriggerContainer] = useState<HTMLElement | null>(
159+
null,
160+
)
121161

122162
const [PluginComponents, setPluginComponents] = useState<
123163
Record<string, JSX.Element>
124164
>({})
125165
const [TitleComponents, setTitleComponents] = useState<
126166
Record<string, JSX.Element>
127167
>({})
168+
const [TriggerComponent, setTriggerComponent] = useState<JSX.Element | null>(
169+
null,
170+
)
128171

129172
const pluginsMap: Array<TanStackDevtoolsPlugin> = useMemo(
130173
() =>
@@ -170,14 +213,22 @@ export const TanStackDevtools = ({
170213
[plugins],
171214
)
172215

173-
const [devtools] = useState(
174-
() =>
175-
new TanStackDevtoolsCore({
176-
config,
177-
eventBusConfig,
178-
plugins: pluginsMap,
179-
}),
180-
)
216+
const [devtools] = useState(() => {
217+
const { customTrigger, ...coreConfig } = config || {}
218+
return new TanStackDevtoolsCore({
219+
config: {
220+
...coreConfig,
221+
customTrigger: customTrigger
222+
? (el, props) => {
223+
setTriggerContainer(el)
224+
convertTrigger(customTrigger, setTriggerComponent, el, props)
225+
}
226+
: undefined,
227+
},
228+
eventBusConfig,
229+
plugins: pluginsMap,
230+
})
231+
})
181232

182233
useEffect(() => {
183234
devtools.setConfig({
@@ -215,6 +266,10 @@ export const TanStackDevtools = ({
215266
createPortal(<>{TitleComponents[key]}</>, titleContainer),
216267
)
217268
: null}
269+
270+
{triggerContainer && TriggerComponent
271+
? createPortal(<>{TriggerComponent}</>, triggerContainer)
272+
: null}
218273
</>
219274
)
220275
}

packages/solid-devtools/src/core.tsx

Lines changed: 30 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,9 @@ export type TanStackDevtoolsSolidPlugin = Omit<
7474
*/
7575
name: string | SolidPluginRender
7676
}
77+
interface TriggerProps {
78+
theme: 'light' | 'dark'
79+
}
7780
export interface TanStackDevtoolsInit {
7881
/**
7982
* Array of plugins to be used in the devtools.
@@ -98,7 +101,14 @@ export interface TanStackDevtoolsInit {
98101
* initial state of the devtools when it is started for the first time. Afterwards,
99102
* the settings are persisted in local storage and changed through the settings panel.
100103
*/
101-
config?: Partial<TanStackDevtoolsConfig>
104+
config?: Omit<Partial<TanStackDevtoolsConfig>, 'customTrigger'> & {
105+
/**
106+
* An optional custom function to render the dev tools trigger component.
107+
*/
108+
customTrigger?:
109+
| ((el: HTMLElement, props: TriggerProps) => JSX.Element)
110+
| JSX.Element
111+
}
102112
/**
103113
* Configuration for the TanStack Devtools client event bus.
104114
*/
@@ -125,17 +135,34 @@ export default function SolidDevtoolsCore({
125135
})),
126136
)
127137

138+
const convertTrigger = (el: HTMLElement, props: TriggerProps) => {
139+
const Trigger = config?.customTrigger
140+
141+
return (
142+
<Portal mount={el}>
143+
{typeof Trigger === 'function' ? Trigger(el, props) : Trigger}
144+
</Portal>
145+
)
146+
}
128147
const [devtools] = createSignal(
129148
new TanStackDevtoolsCore({
130-
config,
149+
config: {
150+
...config,
151+
customTrigger: (el, props) => convertTrigger(el, props),
152+
},
131153
eventBusConfig,
132154
plugins: pluginsMap(),
133155
}),
134156
)
135157
let devToolRef: HTMLDivElement | undefined
136158

137159
createEffect(() => {
138-
devtools().setConfig({ config })
160+
devtools().setConfig({
161+
config: {
162+
...config,
163+
customTrigger: (el, props) => convertTrigger(el, props),
164+
},
165+
})
139166
})
140167

141168
// Update plugins when they change

pnpm-lock.yaml

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)