-
Notifications
You must be signed in to change notification settings - Fork 189
Expand file tree
/
Copy pathNavigationBar.tsx
More file actions
203 lines (193 loc) · 7.71 KB
/
NavigationBar.tsx
File metadata and controls
203 lines (193 loc) · 7.71 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
/*
* Copyright (c) Facebook, Inc. and its affiliates.
*/
import {
useRef,
useInsertionEffect,
useCallback,
useState,
useEffect,
Fragment,
} from 'react';
import cn from 'classnames';
import {
FileTabs,
useSandpack,
useSandpackNavigation,
} from '@codesandbox/sandpack-react/unstyled';
import {OpenInCodeSandboxButton} from './OpenInCodeSandboxButton';
import {ReloadButton} from './ReloadButton';
import {ClearButton} from './ClearButton';
import {DownloadButton} from './DownloadButton';
import {IconChevron} from '../../Icon/IconChevron';
import {Listbox} from '@headlessui/react';
import {OpenInTypeScriptPlaygroundButton} from './OpenInTypeScriptPlayground';
export function useEvent(fn: any): any {
const ref = useRef(null);
useInsertionEffect(() => {
ref.current = fn;
}, [fn]);
return useCallback((...args: any) => {
const f = ref.current!;
// @ts-ignore
return f(...args);
}, []);
}
const getFileName = (filePath: string): string => {
const lastIndexOfSlash = filePath.lastIndexOf('/');
return filePath.slice(lastIndexOfSlash + 1);
};
export function NavigationBar({providedFiles}: {providedFiles: Array<string>}) {
const {sandpack} = useSandpack();
const containerRef = useRef<HTMLDivElement | null>(null);
const tabsRef = useRef<HTMLDivElement | null>(null);
// By default, show the dropdown because all tabs may not fit.
// We don't know whether they'll fit or not until after hydration:
const [showDropdown, setShowDropdown] = useState(true);
const {activeFile, setActiveFile, visibleFiles, clients} = sandpack;
const clientId = Object.keys(clients)[0];
const {refresh} = useSandpackNavigation(clientId);
const isMultiFile = visibleFiles.length > 1;
const hasJustToggledDropdown = useRef(false);
// Keep track of whether we can show all tabs or just the dropdown.
const onContainerResize = useEvent((containerWidth: number) => {
if (hasJustToggledDropdown.current === true) {
// Ignore changes likely caused by ourselves.
hasJustToggledDropdown.current = false;
return;
}
if (tabsRef.current === null) {
// Some ResizeObserver calls come after unmount.
return;
}
const tabsWidth = tabsRef.current.getBoundingClientRect().width;
const needsDropdown = tabsWidth >= containerWidth;
if (needsDropdown !== showDropdown) {
hasJustToggledDropdown.current = true;
setShowDropdown(needsDropdown);
}
});
useEffect(() => {
if (isMultiFile) {
const resizeObserver = new ResizeObserver((entries) => {
for (const entry of entries) {
if (entry.contentBoxSize) {
const contentBoxSize = Array.isArray(entry.contentBoxSize)
? entry.contentBoxSize[0]
: entry.contentBoxSize;
const width = contentBoxSize.inlineSize;
onContainerResize(width);
}
}
});
const container = containerRef.current!;
resizeObserver.observe(container);
return () => resizeObserver.unobserve(container);
} else {
return;
}
// Note: in a real useEvent, onContainerResize would be omitted.
}, [isMultiFile, onContainerResize]);
const handleClear = () => {
/**
* resetAllFiles must come first, otherwise
* the previous content will appear for a second
* when the iframe loads.
*
* Plus, it should only prompt if there's any file changes
*/
if (sandpack.editorState === 'dirty' && confirm('Clear all your edits?')) {
sandpack.resetAllFiles();
}
refresh();
};
const handleReload = () => {
refresh();
};
return (
<div className="bg-wash dark:bg-card-dark flex justify-between items-center relative z-10 border-b border-border dark:border-border-dark rounded-t-lg text-lg">
{/* If Prettier reformats this block, the two @ts-ignore directives will no longer be adjacent to the problematic lines, causing TypeScript errors */}
{/* prettier-ignore */}
<div className="flex-1 grow min-w-0 px-4 lg:px-6">
{/* @ts-ignore: the Listbox type from '@headlessui/react' is incompatible with JSX in React 19 */}
<Listbox value={activeFile} onChange={setActiveFile}>
<div ref={containerRef}>
<div className="relative overflow-hidden">
<div
ref={tabsRef}
className={cn(
// The container for all tabs is always in the DOM, but
// not always visible. This lets us measure how much space
// the tabs would take if displayed. We use this to decide
// whether to keep showing the dropdown, or show all tabs.
'w-[fit-content]',
showDropdown ? 'invisible' : ''
)}>
{/* @ts-ignore: the FileTabs type from '@codesandbox/sandpack-react/unstyled' is incompatible with JSX in React 19 */}
<FileTabs />
</div>
{/* @ts-ignore: the Listbox type from '@headlessui/react' is incompatible with JSX in React 19 */}
<Listbox.Button as={Fragment}>
{({open}) => (
// If tabs don't fit, display the dropdown instead.
// The dropdown is absolutely positioned inside the
// space that's taken by the (invisible) tab list.
<button
className={cn(
'absolute top-0 start-[2px]',
!showDropdown && 'invisible'
)}>
<span
className={cn(
'h-full py-2 px-1 mt-px -mb-px flex border-b text-link dark:text-link-dark border-link dark:border-link-dark items-center text-md leading-tight truncate'
)}
style={{maxWidth: '160px'}}>
{getFileName(activeFile)}
{isMultiFile && (
<span className="ms-2">
<IconChevron
displayDirection={open ? 'up' : 'down'}
/>
</span>
)}
</span>
</button>
)}
</Listbox.Button>
</div>
</div>
{/* @ts-ignore: the Listbox type from '@headlessui/react' is incompatible with JSX in React 19 */}
{isMultiFile && showDropdown && (<Listbox.Options className="absolute mt-0.5 bg-card dark:bg-card-dark px-2 inset-x-0 mx-0 rounded-b-lg border-1 border-border dark:border-border-dark rounded-sm shadow-md">
{/* @ts-ignore: the Listbox type from '@headlessui/react' is incompatible with JSX in React 19 */}
{visibleFiles.map((filePath: string) => (<Listbox.Option key={filePath} value={filePath} as={Fragment}>
{({active}) => (
<li
className={cn(
'text-md mx-2 my-4 cursor-pointer',
active && 'text-link dark:text-link-dark'
)}>
{getFileName(filePath)}
</li>
)}
</Listbox.Option>
))}
</Listbox.Options>
)}
</Listbox>
</div>
<div
className="px-3 flex items-center justify-end text-start"
translate="yes">
<DownloadButton providedFiles={providedFiles} />
<ReloadButton onReload={handleReload} />
<ClearButton onClear={handleClear} />
<OpenInCodeSandboxButton />
{activeFile.endsWith('.tsx') && (
<OpenInTypeScriptPlaygroundButton
content={sandpack.files[activeFile]?.code || ''}
/>
)}
</div>
</div>
);
}