Browse Source

[Beta] Fix layout jump due to sandbox tabs (#5014)

main
dan 2 years ago
committed by GitHub
parent
commit
9914e5e025
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 2
      beta/package.json
  2. 50
      beta/src/components/MDX/Sandpack/FilesDropdown.tsx
  3. 140
      beta/src/components/MDX/Sandpack/NavigationBar.tsx
  4. 2
      beta/src/styles/sandpack.css
  5. 8
      beta/yarn.lock

2
beta/package.json

@ -25,7 +25,7 @@
"@codesandbox/sandpack-react": "v0.19.8-experimental.7",
"@docsearch/css": "3.0.0-alpha.41",
"@docsearch/react": "3.0.0-alpha.41",
"@headlessui/react": "^1.3.0",
"@headlessui/react": "^1.7.0",
"body-scroll-lock": "^3.1.3",
"classnames": "^2.2.6",
"date-fns": "^2.16.1",

50
beta/src/components/MDX/Sandpack/FilesDropdown.tsx

@ -1,50 +0,0 @@
/*
* Copyright (c) Facebook, Inc. and its affiliates.
*/
import * as React from 'react';
import cn from 'classnames';
import {IconChevron} from '../../Icon/IconChevron';
import {useSandpack} from '@codesandbox/sandpack-react';
import {Listbox} from '@headlessui/react';
const getFileName = (filePath: string): string => {
const lastIndexOfSlash = filePath.lastIndexOf('/');
return filePath.slice(lastIndexOfSlash + 1);
};
export function FilesDropdown() {
const {sandpack} = useSandpack();
const {openPaths, setActiveFile, activePath} = sandpack;
return (
<Listbox value={activePath} onChange={setActiveFile}>
<Listbox.Button>
{({open}) => (
<span
className={cn(
'h-full py-2 px-1 mt-px -mb-px flex border-b-2 text-link dark:text-link-dark border-link dark:border-link-dark items-center text-md leading-tight truncate'
)}
style={{maxWidth: '160px'}}>
{getFileName(activePath)}
<span className="ml-2">
<IconChevron displayDirection={open ? 'up' : 'down'} />
</span>
</span>
)}
</Listbox.Button>
<Listbox.Options className="absolute mt-0.5 bg-card dark:bg-card-dark px-2 left-0 right-0 mx-0 rounded-b-lg border-1 border-border dark:border-border-dark rounded-sm shadow-md">
{openPaths.map((filePath: string) => (
<Listbox.Option
key={filePath}
value={filePath}
className={cn(
'text-md mx-2 my-4 cursor-pointer',
filePath === activePath && 'text-link dark:text-link-dark'
)}>
{getFileName(filePath)}
</Listbox.Option>
))}
</Listbox.Options>
</Listbox>
);
}

140
beta/src/components/MDX/Sandpack/NavigationBar.tsx

@ -3,6 +3,7 @@
*/
import * as React from 'react';
import cn from 'classnames';
import {
FileTabs,
useSandpack,
@ -11,35 +12,75 @@ import {
import {OpenInCodeSandboxButton} from './OpenInCodeSandboxButton';
import {ResetButton} from './ResetButton';
import {DownloadButton} from './DownloadButton';
import {FilesDropdown} from './FilesDropdown';
import {IconChevron} from '../../Icon/IconChevron';
import {Listbox} from '@headlessui/react';
// TODO: Replace with real useEvent.
export function useEvent(fn: any): any {
const ref = React.useRef(null);
React.useInsertionEffect(() => {
ref.current = fn;
}, [fn]);
return React.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 [dropdownActive, setDropdownActive] = React.useState(false);
const {openPaths, clients} = sandpack;
const containerRef = React.useRef<HTMLDivElement | null>(null);
const tabsRef = React.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] = React.useState(true);
const {openPaths, clients, setActiveFile, activePath} = sandpack;
const clientId = Object.keys(clients)[0];
const {refresh} = useSandpackNavigation(clientId);
const isMultiFile = openPaths.length > 1;
const hasJustToggledDropdown = React.useRef(false);
const resizeHandler = React.useCallback(() => {
const width = window.innerWidth || document.documentElement.clientWidth;
if (!dropdownActive && width < 640) {
setDropdownActive(true);
// 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 (dropdownActive && width >= 640) {
setDropdownActive(false);
const tabsWidth = tabsRef.current!.getBoundingClientRect().width;
const needsDropdown = tabsWidth >= containerWidth;
if (needsDropdown !== showDropdown) {
hasJustToggledDropdown.current = true;
setShowDropdown(needsDropdown);
}
}, [dropdownActive]);
});
React.useEffect(() => {
if (openPaths.length > 1) {
resizeHandler();
window.addEventListener('resize', resizeHandler);
return () => {
window.removeEventListener('resize', resizeHandler);
};
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;
}, [openPaths.length, resizeHandler]);
}
}, [isMultiFile]);
const handleReset = () => {
sandpack.resetAllFiles();
@ -48,11 +89,70 @@ export function NavigationBar({providedFiles}: {providedFiles: Array<string>}) {
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 rounded-b-none">
<div className="px-4 lg:px-6">
{dropdownActive ? <FilesDropdown /> : <FileTabs />}
<div className="flex-1 grow min-w-0 px-4 lg:px-6">
<Listbox value={activePath} 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' : ''
)}>
<FileTabs />
</div>
<Listbox.Button className="contents">
{({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.
<div
className={cn(
'absolute top-0 left-0',
!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(activePath)}
{isMultiFile && (
<span className="ml-2">
<IconChevron
displayDirection={open ? 'up' : 'down'}
/>
</span>
)}
</span>
</div>
)}
</Listbox.Button>
</div>
</div>
{isMultiFile && showDropdown && (
<Listbox.Options className="absolute mt-0.5 bg-card dark:bg-card-dark px-2 left-0 right-0 mx-0 rounded-b-lg border-1 border-border dark:border-border-dark rounded-sm shadow-md">
{openPaths.map((filePath: string) => (
<Listbox.Option
key={filePath}
value={filePath}
className={cn(
'text-md mx-2 my-4 cursor-pointer',
filePath === activePath && 'text-link dark:text-link-dark'
)}>
{getFileName(filePath)}
</Listbox.Option>
))}
</Listbox.Options>
)}
</Listbox>
</div>
<div
className="px-3 flex items-center justify-end grow text-right"
className="px-3 flex items-center justify-end text-right"
translate="yes">
<DownloadButton providedFiles={providedFiles} />
<ResetButton onReset={handleReset} />

2
beta/src/styles/sandpack.css

@ -57,7 +57,7 @@ html.dark .sp-wrapper {
.sp-tabs .sp-tab-button {
color: #087ea4;
padding: 0 4px;
padding: 0 6px;
border-bottom: 2px solid transparent;
}

8
beta/yarn.lock

@ -878,10 +878,10 @@
minimatch "^3.0.4"
strip-json-comments "^3.1.1"
"@headlessui/react@^1.3.0":
version "1.4.3"
resolved "https://registry.yarnpkg.com/@headlessui/react/-/react-1.4.3.tgz#f77c6bb5cb4a614a5d730fb880cab502d48abf37"
integrity sha512-n2IQkaaw0aAAlQS5MEXsM4uRK+w18CrM72EqnGRl/UBOQeQajad8oiKXR9Nk15jOzTFQjpxzrZMf1NxHidFBiw==
"@headlessui/react@^1.7.0":
version "1.7.0"
resolved "https://registry.yarnpkg.com/@headlessui/react/-/react-1.7.0.tgz#7e36e6bbc25a24b02011527ae157a000dda88b85"
integrity sha512-/nDsijOXRwXVLpUBEiYuWguIBSIN3ZbKyah+KPUiD8bdIKtX1U/k+qLYUEr7NCQnSF2e4w1dr8me42ECuG3cvw==
"@humanwhocodes/config-array@^0.5.0":
version "0.5.0"

Loading…
Cancel
Save