Skip to content

Commit a1fb641

Browse files
reihwaldLiam Reihwald
andauthored
feat(router-devtools): add shadowDOMTarget option to TanStackRouterDevtools and TanStackRouterDevtoolsPanel (#1709) (#1720)
* feat: add shadowDOMTarget option to TanStackRouterDevtools and TanStackRouterDevtoolsPanel (#1709) * feat: add documentation for shadowDOMTarget (#1709) --------- Co-authored-by: Liam Reihwald <liam.reihwald@worldline.com>
1 parent ff8b875 commit a1fb641

File tree

4 files changed

+188
-137
lines changed

4 files changed

+188
-137
lines changed

docs/framework/react/devtools.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,9 @@ function App() {
124124
- `position?: "top-left" | "top-right" | "bottom-left" | "bottom-right"`
125125
- Defaults to `bottom-left`
126126
- The position of the TanStack Router logo to open and close the devtools panel
127+
- `shadowDOMTarget?: ShadowRoot`
128+
- Specifies a Shadow DOM target for the devtools.
129+
- By default, devtool styles are applied to the `<head>` tag of the main document (light DOM). When a `shadowDOMTarget` is provided, styles will be applied within this Shadow DOM instead.
127130

128131
## Embedded Mode
129132

packages/router-devtools/src/Explorer.tsx

Lines changed: 43 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,34 +1,38 @@
11
import * as React from 'react'
22
import { clsx as cx } from 'clsx'
3-
import { css } from 'goober'
3+
import * as goober from 'goober'
44
import { tokens } from './tokens'
55
import { displayValue, styled } from './utils'
6+
import { ShadowDomTargetContext } from './context'
67

78
type ExpanderProps = {
89
expanded: boolean
910
style?: React.CSSProperties
1011
}
1112

12-
export const Expander = ({ expanded, style = {} }: ExpanderProps) => (
13-
<span className={getStyles().expander}>
14-
<svg
15-
xmlns="http://www.w3.org/2000/svg"
16-
width="12"
17-
height="12"
18-
fill="none"
19-
viewBox="0 0 24 24"
20-
className={cx(getStyles().expanderIcon(expanded))}
21-
>
22-
<path
23-
stroke="currentColor"
24-
strokeLinecap="round"
25-
strokeLinejoin="round"
26-
strokeWidth="2"
27-
d="M9 18l6-6-6-6"
28-
></path>
29-
</svg>
30-
</span>
31-
)
13+
export const Expander = ({ expanded, style = {} }: ExpanderProps) => {
14+
const styles = useStyles()
15+
return (
16+
<span className={styles.expander}>
17+
<svg
18+
xmlns="http://www.w3.org/2000/svg"
19+
width="12"
20+
height="12"
21+
fill="none"
22+
viewBox="0 0 24 24"
23+
className={cx(styles.expanderIcon(expanded))}
24+
>
25+
<path
26+
stroke="currentColor"
27+
strokeLinecap="round"
28+
strokeLinejoin="round"
29+
strokeWidth="2"
30+
d="M9 18l6-6-6-6"
31+
></path>
32+
</svg>
33+
</span>
34+
)
35+
}
3236

3337
type Entry = {
3438
label: string
@@ -84,39 +88,40 @@ export const DefaultRenderer: Renderer = ({
8488
}) => {
8589
const [expandedPages, setExpandedPages] = React.useState<Array<number>>([])
8690
const [valueSnapshot, setValueSnapshot] = React.useState(undefined)
91+
const styles = useStyles()
8792

8893
const refreshValueSnapshot = () => {
8994
setValueSnapshot((value as () => any)())
9095
}
9196

9297
return (
93-
<div className={getStyles().entry}>
98+
<div className={styles.entry}>
9499
{subEntryPages.length ? (
95100
<>
96101
<button
97-
className={getStyles().expandButton}
102+
className={styles.expandButton}
98103
onClick={() => toggleExpanded()}
99104
>
100105
<Expander expanded={expanded} />
101106
{label}
102-
<span className={getStyles().info}>
107+
<span className={styles.info}>
103108
{String(type).toLowerCase() === 'iterable' ? '(Iterable) ' : ''}
104109
{subEntries.length} {subEntries.length > 1 ? `items` : `item`}
105110
</span>
106111
</button>
107112
{expanded ? (
108113
subEntryPages.length === 1 ? (
109-
<div className={getStyles().subEntries}>
114+
<div className={styles.subEntries}>
110115
{subEntries.map((entry, index) => handleEntry(entry))}
111116
</div>
112117
) : (
113-
<div className={getStyles().subEntries}>
118+
<div className={styles.subEntries}>
114119
{subEntryPages.map((entries, index) => {
115120
return (
116121
<div key={index}>
117-
<div className={getStyles().entry}>
122+
<div className={styles.entry}>
118123
<button
119-
className={cx(getStyles().labelButton, 'labelButton')}
124+
className={cx(styles.labelButton, 'labelButton')}
120125
onClick={() =>
121126
setExpandedPages((old) =>
122127
old.includes(index)
@@ -130,7 +135,7 @@ export const DefaultRenderer: Renderer = ({
130135
{index * pageSize + pageSize - 1}]
131136
</button>
132137
{expandedPages.includes(index) ? (
133-
<div className={getStyles().subEntries}>
138+
<div className={styles.subEntries}>
134139
{entries.map((entry) => handleEntry(entry))}
135140
</div>
136141
) : null}
@@ -149,7 +154,7 @@ export const DefaultRenderer: Renderer = ({
149154
label={
150155
<button
151156
onClick={refreshValueSnapshot}
152-
className={getStyles().refreshValueBtn}
157+
className={styles.refreshValueBtn}
153158
>
154159
<span>{label}</span> 🔄{' '}
155160
</button>
@@ -161,7 +166,7 @@ export const DefaultRenderer: Renderer = ({
161166
) : (
162167
<>
163168
<span>{label}:</span>{' '}
164-
<span className={getStyles().value}>{displayValue(value)}</span>
169+
<span className={styles.value}>{displayValue(value)}</span>
165170
</>
166171
)}
167172
</div>
@@ -267,9 +272,12 @@ export default function Explorer({
267272
})
268273
}
269274

270-
const stylesFactory = () => {
275+
const stylesFactory = (shadowDOMTarget?: ShadowRoot) => {
271276
const { colors, font, size, alpha, shadow, border } = tokens
272277
const { fontFamily, lineHeight, size: fontSize } = font
278+
const css = shadowDOMTarget
279+
? goober.css.bind({ target: shadowDOMTarget })
280+
: goober.css
273281

274282
return {
275283
entry: css`
@@ -349,9 +357,10 @@ const stylesFactory = () => {
349357

350358
let _styles: ReturnType<typeof stylesFactory> | null = null
351359

352-
function getStyles() {
360+
function useStyles() {
361+
const shadowDomTarget = React.useContext(ShadowDomTargetContext)
353362
if (_styles) return _styles
354-
_styles = stylesFactory()
363+
_styles = stylesFactory(shadowDomTarget)
355364

356365
return _styles
357366
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import React from 'react'
2+
3+
export const ShadowDomTargetContext = React.createContext<
4+
ShadowRoot | undefined
5+
>(undefined)
6+
7+
export const DevtoolsOnCloseContext = React.createContext<
8+
| {
9+
onCloseClick: (e: React.MouseEvent<HTMLButtonElement>) => void
10+
}
11+
| undefined
12+
>(undefined)
13+
14+
export const useDevtoolsOnClose = () => {
15+
const context = React.useContext(DevtoolsOnCloseContext)
16+
if (!context) {
17+
throw new Error(
18+
'useDevtoolsOnClose must be used within a TanStackRouterDevtools component',
19+
)
20+
}
21+
return context
22+
}

0 commit comments

Comments
 (0)