From 9914e5e0253ec6e0840bdfcf139a47275a11b721 Mon Sep 17 00:00:00 2001 From: dan Date: Wed, 7 Sep 2022 21:24:31 +0100 Subject: [PATCH] [Beta] Fix layout jump due to sandbox tabs (#5014) --- beta/package.json | 2 +- .../components/MDX/Sandpack/FilesDropdown.tsx | 50 ------ .../components/MDX/Sandpack/NavigationBar.tsx | 142 +++++++++++++++--- beta/src/styles/sandpack.css | 2 +- beta/yarn.lock | 8 +- 5 files changed, 127 insertions(+), 77 deletions(-) delete mode 100644 beta/src/components/MDX/Sandpack/FilesDropdown.tsx diff --git a/beta/package.json b/beta/package.json index 8b38bb59..af88884e 100644 --- a/beta/package.json +++ b/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", diff --git a/beta/src/components/MDX/Sandpack/FilesDropdown.tsx b/beta/src/components/MDX/Sandpack/FilesDropdown.tsx deleted file mode 100644 index 68d068ad..00000000 --- a/beta/src/components/MDX/Sandpack/FilesDropdown.tsx +++ /dev/null @@ -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 ( - - - {({open}) => ( - - {getFileName(activePath)} - - - - - )} - - - {openPaths.map((filePath: string) => ( - - {getFileName(filePath)} - - ))} - - - ); -} diff --git a/beta/src/components/MDX/Sandpack/NavigationBar.tsx b/beta/src/components/MDX/Sandpack/NavigationBar.tsx index 1f0ce64e..9adf0fca 100644 --- a/beta/src/components/MDX/Sandpack/NavigationBar.tsx +++ b/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}) { const {sandpack} = useSandpack(); - const [dropdownActive, setDropdownActive] = React.useState(false); - const {openPaths, clients} = sandpack; + const containerRef = React.useRef(null); + const tabsRef = React.useRef(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; } - return; - }, [openPaths.length, resizeHandler]); + }, [isMultiFile]); const handleReset = () => { sandpack.resetAllFiles(); @@ -48,11 +89,70 @@ export function NavigationBar({providedFiles}: {providedFiles: Array}) { return (
-
- {dropdownActive ? : } +
+ +
+
+
+ +
+ + {({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. +
+ + {getFileName(activePath)} + {isMultiFile && ( + + + + )} + +
+ )} +
+
+
+ {isMultiFile && showDropdown && ( + + {openPaths.map((filePath: string) => ( + + {getFileName(filePath)} + + ))} + + )} +
diff --git a/beta/src/styles/sandpack.css b/beta/src/styles/sandpack.css index 1867704b..4209bda8 100644 --- a/beta/src/styles/sandpack.css +++ b/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; } diff --git a/beta/yarn.lock b/beta/yarn.lock index 4f14fa54..0330a549 100644 --- a/beta/yarn.lock +++ b/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"