Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .changeset/dirty-berries-mate.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'@tanstack/react-hotkeys-devtools': patch
'@tanstack/preact-hotkeys-devtools': patch
'@tanstack/solid-hotkeys-devtools': patch
---

Allow standalone HotkeysDevtoolsPanel components without props, supplying dark theme and open defaults while preserving props from the Devtools dock.
14 changes: 14 additions & 0 deletions docs/devtools.md
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,20 @@ import { HotkeysDevtoolsPanel } from '@tanstack/vue-hotkeys-devtools'

For React, Preact, and Solid, the Hotkeys panel appears alongside any other TanStack devtools plugins you have installed.

### Standalone panel

React, Preact, and Solid also export `HotkeysDevtoolsPanel` for rendering without the TanStack Devtools dock:

```tsx
import { HotkeysDevtoolsPanel } from '@tanstack/react-hotkeys-devtools'

function DebugPanel() {
return <HotkeysDevtoolsPanel />
}
```

Use the corresponding `@tanstack/preact-hotkeys-devtools` or `@tanstack/solid-hotkeys-devtools` import for those frameworks. The panel and no-op component also accept an omitted props argument during rendering. Both props are optional: `theme` defaults to `'dark'` and `devtoolsOpen` defaults to `true`. Pass `theme="light"` to select the light theme. When using `hotkeysDevtoolsPlugin()`, the dock supplies these props and its values take precedence over the standalone defaults.

## Production builds

In production builds, the framework devtools adapters return no-op implementations, so they don't affect your bundle's behavior.
Expand Down
20 changes: 16 additions & 4 deletions packages/preact-hotkeys-devtools/src/PreactHotkeysDevtools.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,27 @@
import { h } from 'preact'
import { createPreactPanel } from '@tanstack/devtools-utils/preact'
import { HotkeysDevtoolsCore } from '@tanstack/hotkeys-devtools'
import type { DevtoolsPanelProps } from '@tanstack/devtools-utils/preact'
import type { JSX } from 'preact'

export interface HotkeysDevtoolsPreactInit extends DevtoolsPanelProps {}
export interface HotkeysDevtoolsPreactInit extends Partial<DevtoolsPanelProps> {}

type HotkeysDevtoolsPanelComponent = (
props: HotkeysDevtoolsPreactInit,
props?: HotkeysDevtoolsPreactInit,
) => JSX.Element

const panels = createPreactPanel(HotkeysDevtoolsCore)

export const HotkeysDevtoolsPanel: HotkeysDevtoolsPanelComponent = panels[0]
export const HotkeysDevtoolsPanelNoOp: HotkeysDevtoolsPanelComponent = panels[1]
function withDefaults(
Panel: (typeof panels)[number],
): HotkeysDevtoolsPanelComponent {
return (props = {}) =>
h(Panel, {
...props,
theme: props.theme ?? 'dark',
devtoolsOpen: props.devtoolsOpen ?? true,
})
}

export const HotkeysDevtoolsPanel = withDefaults(panels[0])
export const HotkeysDevtoolsPanelNoOp = withDefaults(panels[1])
67 changes: 67 additions & 0 deletions packages/preact-hotkeys-devtools/tests/panel.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import { h, render } from 'preact'
import { act } from 'preact/test-utils'
import { afterEach, beforeEach, expect, it, vi } from 'vitest'
import {
HotkeysDevtoolsPanel,
HotkeysDevtoolsPanelNoOp,
} from '../src/PreactHotkeysDevtools'
import { hotkeysDevtoolsPlugin } from '../src/plugin'

const core = vi.hoisted(() => ({ mount: vi.fn(), unmount: vi.fn() }))
vi.mock('@tanstack/hotkeys-devtools', () => ({
HotkeysDevtoolsCore: class {
mount = core.mount
unmount = core.unmount
},
}))

const emptyProps: NonNullable<Parameters<typeof HotkeysDevtoolsPanel>[0]> = {}
let host: HTMLDivElement
beforeEach(() => {
vi.clearAllMocks()
host = document.createElement('div')
document.body.append(host)
})
afterEach(() => {
act(() => render(null, host))
host.remove()
})

it('mounts a standalone panel with default props', () => {
act(() => render(h(HotkeysDevtoolsPanel, emptyProps), host))
expect(core.mount).toHaveBeenCalledOnce()
expect(core.mount.mock.calls[0]![1]).toMatchObject({
theme: 'dark',
devtoolsOpen: true,
})
act(() => render(null, host))
expect(core.unmount).toHaveBeenCalledOnce()
})

it('preserves props supplied by the devtools dock', () => {
const plugin = hotkeysDevtoolsPlugin()
act(() =>
render(plugin.render(host, { theme: 'light', devtoolsOpen: false }), host),
)
expect(core.mount.mock.calls[0]![1]).toMatchObject({
theme: 'light',
devtoolsOpen: false,
})
})

it('keeps the no-op panel inert with empty props', () => {
act(() => render(h(HotkeysDevtoolsPanelNoOp, emptyProps), host))
expect(core.mount).not.toHaveBeenCalled()
})

it('accepts omitted props for real and no-op panels during render', () => {
const Standalone = () => HotkeysDevtoolsPanel()
const NoOp = () => HotkeysDevtoolsPanelNoOp()
act(() => render(h(Standalone, {}), host))
expect(core.mount.mock.calls[0]![1]).toMatchObject({
theme: 'dark',
devtoolsOpen: true,
})
act(() => render(h(NoOp, {}), host))
expect(core.mount).toHaveBeenCalledOnce()
})
20 changes: 16 additions & 4 deletions packages/react-hotkeys-devtools/src/ReactHotkeysDevtools.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,27 @@
import { createElement } from 'react'
import { createReactPanel } from '@tanstack/devtools-utils/react'
import { HotkeysDevtoolsCore } from '@tanstack/hotkeys-devtools'
import type { DevtoolsPanelProps } from '@tanstack/devtools-utils/react'
import type { JSX } from 'react'

export interface HotkeysDevtoolsReactInit extends DevtoolsPanelProps {}
export interface HotkeysDevtoolsReactInit extends Partial<DevtoolsPanelProps> {}

type HotkeysDevtoolsPanelComponent = (
props: HotkeysDevtoolsReactInit,
props?: HotkeysDevtoolsReactInit,
) => JSX.Element

const panels = createReactPanel(HotkeysDevtoolsCore)

export const HotkeysDevtoolsPanel: HotkeysDevtoolsPanelComponent = panels[0]
export const HotkeysDevtoolsPanelNoOp: HotkeysDevtoolsPanelComponent = panels[1]
function withDefaults(
Panel: (typeof panels)[number],
): HotkeysDevtoolsPanelComponent {
return (props = {}) =>
createElement(Panel, {
...props,
theme: props.theme ?? 'dark',
devtoolsOpen: props.devtoolsOpen ?? true,
})
}

export const HotkeysDevtoolsPanel = withDefaults(panels[0])
export const HotkeysDevtoolsPanelNoOp = withDefaults(panels[1])
75 changes: 75 additions & 0 deletions packages/react-hotkeys-devtools/tests/panel.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import { act, createElement } from 'react'
import { createRoot } from 'react-dom/client'
import { afterEach, beforeEach, expect, it, vi } from 'vitest'
import {
HotkeysDevtoolsPanel,
HotkeysDevtoolsPanelNoOp,
} from '../src/ReactHotkeysDevtools'
import { hotkeysDevtoolsPlugin } from '../src/plugin'
import type { Root } from 'react-dom/client'

const core = vi.hoisted(() => ({ mount: vi.fn(), unmount: vi.fn() }))
vi.mock('@tanstack/hotkeys-devtools', () => ({
HotkeysDevtoolsCore: class {
mount = core.mount
unmount = core.unmount
},
}))

const emptyProps = {} satisfies NonNullable<
Parameters<typeof HotkeysDevtoolsPanel>[0]
>
let host: HTMLDivElement
let root: Root
beforeEach(() => {
vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true)
vi.clearAllMocks()
host = document.createElement('div')
document.body.append(host)
root = createRoot(host)
})
afterEach(async () => {
await act(() => root.unmount())
host.remove()
vi.unstubAllGlobals()
})

it('mounts a standalone panel with default props', async () => {
await act(() => root.render(createElement(HotkeysDevtoolsPanel, emptyProps)))
expect(core.mount).toHaveBeenCalledOnce()
expect(core.mount.mock.calls[0]![1]).toMatchObject({
theme: 'dark',
devtoolsOpen: true,
})
await act(() => root.render(null))
})

it('preserves props supplied by the devtools dock', async () => {
const plugin = hotkeysDevtoolsPlugin()
await act(() =>
root.render(plugin.render(host, { theme: 'light', devtoolsOpen: false })),
)
expect(core.mount.mock.calls[0]![1]).toMatchObject({
theme: 'light',
devtoolsOpen: false,
})
})

it('keeps the no-op panel inert with empty props', async () => {
await act(() =>
root.render(createElement(HotkeysDevtoolsPanelNoOp, emptyProps)),
)
expect(core.mount).not.toHaveBeenCalled()
})

it('accepts omitted props for real and no-op panels during render', async () => {
const Standalone = () => HotkeysDevtoolsPanel()
const NoOp = () => HotkeysDevtoolsPanelNoOp()
await act(() => root.render(createElement(Standalone)))
expect(core.mount.mock.calls[0]![1]).toMatchObject({
theme: 'dark',
devtoolsOpen: true,
})
await act(() => root.render(createElement(NoOp)))
expect(core.mount).toHaveBeenCalledOnce()
})
19 changes: 15 additions & 4 deletions packages/solid-hotkeys-devtools/src/SolidHotkeysDevtools.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,26 @@
import { createComponent, mergeProps } from 'solid-js'
import { createSolidPanel } from '@tanstack/devtools-utils/solid'
import { HotkeysDevtoolsCore } from '@tanstack/hotkeys-devtools'
import type { DevtoolsPanelProps } from '@tanstack/devtools-utils/solid'
import type { JSX } from 'solid-js'

export interface HotkeysDevtoolsSolidInit extends DevtoolsPanelProps {}
export interface HotkeysDevtoolsSolidInit extends Partial<DevtoolsPanelProps> {}

type HotkeysDevtoolsPanelComponent = (
props: HotkeysDevtoolsSolidInit,
props?: HotkeysDevtoolsSolidInit,
) => JSX.Element

const panels = createSolidPanel(HotkeysDevtoolsCore)

export const HotkeysDevtoolsPanel: HotkeysDevtoolsPanelComponent = panels[0]
export const HotkeysDevtoolsPanelNoOp: HotkeysDevtoolsPanelComponent = panels[1]
function withDefaults(
Panel: (typeof panels)[number],
): HotkeysDevtoolsPanelComponent {
return (props = {}) =>
createComponent(
Panel,
mergeProps({ theme: 'dark' as const, devtoolsOpen: true }, props),
)
}

export const HotkeysDevtoolsPanel = withDefaults(panels[0])
export const HotkeysDevtoolsPanelNoOp = withDefaults(panels[1])
82 changes: 82 additions & 0 deletions packages/solid-hotkeys-devtools/tests/panel.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import { createComponent, createSignal } from 'solid-js'
import { render } from 'solid-js/web'
import { afterEach, beforeEach, expect, it, vi } from 'vitest'
import {
HotkeysDevtoolsPanel,
HotkeysDevtoolsPanelNoOp,
} from '../src/SolidHotkeysDevtools'
import { hotkeysDevtoolsPlugin } from '../src/plugin'

const core = vi.hoisted(() => ({ mount: vi.fn(), unmount: vi.fn() }))
vi.mock('@tanstack/hotkeys-devtools', () => ({
HotkeysDevtoolsCore: class {
mount = core.mount
unmount = core.unmount
},
}))

const emptyProps = {} satisfies NonNullable<
Parameters<typeof HotkeysDevtoolsPanel>[0]
>
let host: HTMLDivElement
let dispose: () => void
beforeEach(() => {
vi.clearAllMocks()
host = document.createElement('div')
document.body.append(host)
dispose = () => {}
})
afterEach(() => {
dispose()
host.remove()
})

it('mounts a standalone panel with default props', () => {
dispose = render(
() => createComponent(HotkeysDevtoolsPanel, emptyProps),
host,
)
expect(core.mount).toHaveBeenCalledOnce()
expect(core.mount.mock.calls[0]![1]).toMatchObject({
theme: 'dark',
devtoolsOpen: true,
})
})

it('preserves reactive props supplied by the devtools dock', () => {
const [theme, setTheme] = createSignal<'light' | 'dark'>('light')
const plugin = hotkeysDevtoolsPlugin()
dispose = render(
() =>
plugin.render(host, {
get theme() {
return theme()
},
devtoolsOpen: false,
}),
host,
)
const props = core.mount.mock.calls[0]![1]
expect(props).toMatchObject({ theme: 'light', devtoolsOpen: false })
setTheme('dark')
expect(props.theme).toBe('dark')
})

it('keeps the no-op panel inert with empty props', () => {
dispose = render(
() => createComponent(HotkeysDevtoolsPanelNoOp, emptyProps),
host,
)
expect(core.mount).not.toHaveBeenCalled()
})

it('accepts omitted props for real and no-op panels during render', () => {
dispose = render(() => HotkeysDevtoolsPanel(), host)
expect(core.mount.mock.calls[0]![1]).toMatchObject({
theme: 'dark',
devtoolsOpen: true,
})
dispose()
dispose = render(() => HotkeysDevtoolsPanelNoOp(), host)
expect(core.mount).toHaveBeenCalledOnce()
})
Loading