Skip to content

Commit e8e19f1

Browse files
fix: Preview and code view clipping with toolbar open (#1999)
Co-authored-by: Zeh Fernandes <[email protected]>
1 parent 87734ad commit e8e19f1

File tree

10 files changed

+105
-70
lines changed

10 files changed

+105
-70
lines changed

packages/react-email/src/app/layout.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ const RootLayout = async ({ children }: { children: React.ReactNode }) => {
2727
className={`${inter.variable} ${sfMono.variable} font-sans`}
2828
lang="en"
2929
>
30-
<body className="relative flex h-screen flex-col overflow-x-hidden bg-black text-slate-11 leading-loose selection:bg-cyan-5 selection:text-cyan-12">
30+
<body className="relative flex h-screen flex-col bg-black text-slate-11 leading-loose selection:bg-cyan-5 selection:text-cyan-12">
3131
<EmailsProvider
3232
initialEmailsDirectoryMetadata={emailsDirectoryMetadata}
3333
>

packages/react-email/src/app/page.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import Image from 'next/image';
33
import Link from 'next/link';
44
import { Button, Heading, Text } from '../components';
55
import CodeSnippet from '../components/code-snippet';
6-
import { Shell, ShellContent } from '../components/shell';
6+
import { Shell } from '../components/shell';
77
import { emailsDirectoryAbsolutePath } from './env';
88
import logo from './logo.png';
99

@@ -12,8 +12,8 @@ const Home = () => {
1212

1313
return (
1414
<Shell>
15-
<ShellContent className="mx-auto flex max-w-lg items-center justify-center p-8">
16-
<div className="-mt-10 relative flex flex-col items-center gap-3 text-center">
15+
<div className="w-full h-full flex items-center justify-center p-8">
16+
<div className="-mt-10 relative max-w-lg flex flex-col items-center gap-3 text-center">
1717
<Image
1818
alt="React Email Icon"
1919
className="mb-8"
@@ -38,7 +38,7 @@ const Home = () => {
3838
<Link href="https://react.email/docs">Check the docs</Link>
3939
</Button>
4040
</div>
41-
</ShellContent>
41+
</div>
4242
</Shell>
4343
);
4444
};

packages/react-email/src/app/preview/[...slug]/preview.tsx

Lines changed: 33 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,31 @@
11
'use client';
22

33
import { usePathname, useRouter, useSearchParams } from 'next/navigation';
4-
import { use, useRef } from 'react';
4+
import { use, useState } from 'react';
55
import { flushSync } from 'react-dom';
66
import { Toaster } from 'sonner';
77
import { useDebouncedCallback } from 'use-debounce';
88
import { Topbar } from '../../../components';
99
import { CodeContainer } from '../../../components/code-container';
1010
import {
11-
ResizableWarpper,
11+
ResizableWrapper,
1212
makeIframeDocumentBubbleEvents,
1313
} from '../../../components/resizable-wrapper';
1414
import { Send } from '../../../components/send';
15-
import { ShellContent } from '../../../components/shell';
15+
import { useToolbarState } from '../../../components/toolbar';
1616
import { Tooltip } from '../../../components/tooltip';
1717
import { ActiveViewToggleGroup } from '../../../components/topbar/active-view-toggle-group';
1818
import { ViewSizeControls } from '../../../components/topbar/view-size-controls';
1919
import { PreviewContext } from '../../../contexts/preview';
2020
import { useClampedState } from '../../../hooks/use-clamped-state';
21+
import { cn } from '../../../utils';
2122
import { RenderingError } from './rendering-error';
2223

23-
interface PreviewProps {
24+
interface PreviewProps extends React.ComponentProps<'div'> {
2425
emailTitle: string;
2526
}
2627

27-
const Preview = ({ emailTitle }: PreviewProps) => {
28+
const Preview = ({ emailTitle, className, ...props }: PreviewProps) => {
2829
const { renderingResult, renderedEmailMetadata } = use(PreviewContext)!;
2930

3031
const router = useRouter();
@@ -53,21 +54,21 @@ const Preview = ({ emailTitle }: PreviewProps) => {
5354
const hasRenderingMetadata = typeof renderedEmailMetadata !== 'undefined';
5455
const hasErrors = 'error' in renderingResult;
5556

56-
const maxWidthRef = useRef(Number.POSITIVE_INFINITY);
57-
const maxHeightRef = useRef(Number.POSITIVE_INFINITY);
58-
const minWidth = 350;
59-
const minHeight = 600;
57+
const [maxWidth, setMaxWidth] = useState(Number.POSITIVE_INFINITY);
58+
const [maxHeight, setMaxHeight] = useState(Number.POSITIVE_INFINITY);
59+
const minWidth = 100;
60+
const minHeight = 100;
6061
const storedWidth = searchParams.get('width');
6162
const storedHeight = searchParams.get('height');
6263
const [width, setWidth] = useClampedState(
6364
storedWidth ? Number.parseInt(storedWidth) : 600,
64-
350,
65-
maxWidthRef.current,
65+
minWidth,
66+
maxWidth,
6667
);
6768
const [height, setHeight] = useClampedState(
6869
storedHeight ? Number.parseInt(storedHeight) : 1024,
69-
600,
70-
maxHeightRef.current,
70+
minHeight,
71+
maxHeight,
7172
);
7273

7374
const handleSaveViewSize = useDebouncedCallback(() => {
@@ -77,6 +78,8 @@ const Preview = ({ emailTitle }: PreviewProps) => {
7778
router.push(`${pathname}?${params.toString()}${location.hash}`);
7879
}, 300);
7980

81+
const { toggled: toolbarToggled } = useToolbarState();
82+
8083
return (
8184
<>
8285
<Topbar emailTitle={emailTitle}>
@@ -107,14 +110,20 @@ const Preview = ({ emailTitle }: PreviewProps) => {
107110
) : null}
108111
</Topbar>
109112

110-
<ShellContent
111-
className="relative flex bg-gray-200 p-4"
113+
<div
114+
{...props}
115+
className={cn(
116+
'h-[calc(100%-3.5rem-2.375rem)] will-change-height flex p-4 transition-all duration-300',
117+
activeView === 'preview' && 'bg-gray-200',
118+
toolbarToggled && 'h-[calc(100%-3.5rem-13rem)]',
119+
className,
120+
)}
112121
ref={(element) => {
113122
const observer = new ResizeObserver((entry) => {
114123
const [elementEntry] = entry;
115124
if (elementEntry) {
116-
maxWidthRef.current = elementEntry.contentRect.width;
117-
maxHeightRef.current = elementEntry.contentRect.height;
125+
setMaxWidth(elementEntry.contentRect.width);
126+
setMaxHeight(elementEntry.contentRect.height);
118127
}
119128
});
120129

@@ -132,11 +141,11 @@ const Preview = ({ emailTitle }: PreviewProps) => {
132141
{hasRenderingMetadata ? (
133142
<>
134143
{activeView === 'preview' && (
135-
<ResizableWarpper
144+
<ResizableWrapper
136145
minHeight={minHeight}
137146
minWidth={minWidth}
138-
maxHeight={maxHeightRef.current}
139-
maxWidth={maxWidthRef.current}
147+
maxHeight={maxHeight}
148+
maxWidth={maxWidth}
140149
height={height}
141150
onResizeEnd={() => {
142151
handleSaveViewSize();
@@ -166,12 +175,12 @@ const Preview = ({ emailTitle }: PreviewProps) => {
166175
}}
167176
title={emailTitle}
168177
/>
169-
</ResizableWarpper>
178+
</ResizableWrapper>
170179
)}
171180

172181
{activeView === 'source' && (
173-
<div className="h-full w-full bg-black">
174-
<div className="m-auto flex max-w-3xl p-6">
182+
<div className="h-full w-full">
183+
<div className="m-auto h-full flex max-w-3xl p-6">
175184
<Tooltip.Provider>
176185
<CodeContainer
177186
activeLang={activeLang}
@@ -199,7 +208,7 @@ const Preview = ({ emailTitle }: PreviewProps) => {
199208
) : null}
200209

201210
<Toaster />
202-
</ShellContent>
211+
</div>
203212
</>
204213
);
205214
};

packages/react-email/src/components/code-container.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ export const CodeContainer: React.FC<Readonly<CodeContainerProps>> = ({
3939

4040
return (
4141
<div
42-
className="relative w-full items-center whitespace-pre rounded-md border border-slate-6 text-sm"
42+
className="relative max-h-[650px] w-full h-full whitespace-pre rounded-md border border-slate-6 text-sm"
4343
style={{
4444
lineHeight: '130%',
4545
background:
@@ -84,7 +84,7 @@ export const CodeContainer: React.FC<Readonly<CodeContainerProps>> = ({
8484
filename={`email.${activeMarkup.language}`}
8585
/>
8686
</div>
87-
<div>
87+
<div className="h-[calc(100%-2.25rem)]">
8888
<Code language={activeLang}>{activeMarkup.content}</Code>
8989
</div>
9090
</div>

packages/react-email/src/components/code.tsx

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import Link from 'next/link';
33
import { useSearchParams } from 'next/navigation';
44
import type { Language } from 'prism-react-renderer';
55
import { Highlight } from 'prism-react-renderer';
6-
import { Fragment, useEffect } from 'react';
6+
import { Fragment, useEffect, useRef } from 'react';
77
import { useFragmentIdentifier } from '../hooks/use-fragment-identifier';
88
import { cn } from '../utils';
99

@@ -73,12 +73,18 @@ export const Code: React.FC<Readonly<CodeProps>> = ({
7373
return highlight[0] <= line && highlight[1] >= line;
7474
};
7575

76+
const scrollerRef = useRef<HTMLDivElement>(null);
77+
7678
useEffect(() => {
77-
if (highlight) {
78-
document.getElementById(`L${highlight[0]}`)?.scrollIntoView({
79-
block: 'start',
80-
behavior: 'smooth',
81-
});
79+
const scroller = scrollerRef.current
80+
if (highlight && scroller) {
81+
const lineElement = scroller.querySelector(`#L${highlight[0]}`);
82+
if (lineElement instanceof HTMLAnchorElement) {
83+
scroller.scrollTo({
84+
top: lineElement.offsetTop,
85+
behavior: 'smooth',
86+
});
87+
}
8288
}
8389
}, [highlight]);
8490

@@ -97,7 +103,10 @@ export const Code: React.FC<Readonly<CodeProps>> = ({
97103
'linear-gradient(90deg, rgba(56, 189, 248, 0) 0%, rgba(56, 189, 248, 0) 0%, rgba(232, 232, 232, 0.2) 33.02%, rgba(143, 143, 143, 0.6719) 64.41%, rgba(236, 72, 153, 0) 98.93%)',
98104
}}
99105
/>
100-
<div className="flex h-[650px] p-4 max-h-[calc(100vh-10rem)] after:w-full after:static after:block after:h-4 after:content-[''] overflow-auto">
106+
<div
107+
ref={scrollerRef}
108+
className="flex max-h-[650px] h-full p-4 after:w-full after:static after:block after:h-4 after:content-[''] overflow-auto"
109+
>
101110
<div className="text-[#49494f] text-[13px] font-light font-[MonoLisa,_Menlo,_monospace]">
102111
{tokens.map((_, i) => (
103112
<Link

packages/react-email/src/components/resizable-wrapper.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { cn } from '../utils';
44

55
type Direction = 'north' | 'south' | 'east' | 'west';
66

7-
type ResizableWarpperProps = {
7+
type ResizableWrapperProps = {
88
width: number;
99
height: number;
1010

@@ -41,7 +41,7 @@ export const makeIframeDocumentBubbleEvents = (iframe: HTMLIFrameElement) => {
4141
};
4242
};
4343

44-
export const ResizableWarpper = ({
44+
export const ResizableWrapper = ({
4545
width,
4646
height,
4747
onResize,
@@ -54,7 +54,7 @@ export const ResizableWarpper = ({
5454
minWidth,
5555

5656
...rest
57-
}: ResizableWarpperProps) => {
57+
}: ResizableWrapperProps) => {
5858
const resizableRef = useRef<HTMLElement>(null);
5959

6060
const mouseMoveListener = useRef<(event: MouseEvent) => void>(null);

packages/react-email/src/components/shell.tsx

Lines changed: 13 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -62,35 +62,31 @@ export const Shell = ({ children, currentEmailOpenSlug }: ShellProps) => {
6262
</svg>
6363
</button>
6464
</div>
65-
<div className="flex w-[100dvw] h-[100dvh] flex-row">
65+
<div className="w-[100dvw] flex h-[calc(100dvh-4.375rem)] lg:h-[100dvh]">
6666
<React.Suspense>
6767
<Sidebar
68-
className={cn('shrink [transition:width_0.2s_ease-in-out]', {
69-
'-translate-x-full lg:translate-x-0': sidebarToggled,
70-
'lg:w-0': !sidebarToggled,
71-
})}
68+
className={cn(
69+
'fixed top-[4.375rem] left-0 z-[9999] h-full max-h-full w-full max-w-full will-change-auto [transition:width_0.2s_ease-in-out]',
70+
'lg:static lg:inline-block lg:z-auto lg:max-h-full lg:w-[16rem]',
71+
{
72+
'-translate-x-full lg:translate-x-0': sidebarToggled,
73+
'lg:w-0': !sidebarToggled,
74+
},
75+
)}
7276
currentEmailOpenSlug={currentEmailOpenSlug}
7377
/>
7478
</React.Suspense>
7579
<main
7680
className={cn(
77-
'h-full max-h-full min-h-full overflow-hidden will-change-width lg:mt-0 w-full',
81+
'inline-block relative overflow-hidden will-change-width',
82+
'w-full h-full',
7883
'[transition:width_0.2s_ease-in-out,_transform_0.2s_ease-in-out]',
84+
sidebarToggled && 'lg:w-[calc(100%-16rem)]',
7985
)}
8086
>
81-
<div className="relative flex h-full w-full flex-col">{children}</div>
87+
{children}
8288
</main>
8389
</div>
8490
</ShellContext.Provider>
8591
);
8692
};
87-
88-
type ShellContentRootProps = React.ComponentProps<'div'>;
89-
90-
export const ShellContent = ({ children, ...rest }: ShellContentRootProps) => {
91-
return (
92-
<div {...rest} className={cn('relative grow', rest.className)}>
93-
{children}
94-
</div>
95-
);
96-
};

packages/react-email/src/components/sidebar/sidebar.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ export const Sidebar = ({ className, currentEmailOpenSlug }: SidebarProps) => {
1616
return (
1717
<aside
1818
className={cn(
19-
'fixed top-[4.375rem] left-0 z-[9999] h-full max-h-full w-screen max-w-full bg-black will-change-auto',
19+
'overflow-hidden bg-black',
2020
'lg:static lg:z-auto lg:max-h-screen lg:w-[16rem]',
2121
className,
2222
)}

packages/react-email/src/components/toolbar.tsx

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,19 @@ import { useCachedState } from './toolbar/use-cached-state';
2323

2424
export type ToolbarTabValue = 'linter' | 'compatibility' | 'spam-assassin';
2525

26+
export const useToolbarState = () => {
27+
const searchParams = useSearchParams();
28+
const activeTab = (searchParams.get('toolbar-panel') ?? undefined) as
29+
| ToolbarTabValue
30+
| undefined;
31+
32+
return {
33+
activeTab,
34+
35+
toggled: activeTab !== undefined,
36+
};
37+
};
38+
2639
const ToolbarInner = ({
2740
serverLintingRows,
2841
serverSpamCheckingResult,
@@ -44,11 +57,7 @@ const ToolbarInner = ({
4457
const searchParams = useSearchParams();
4558
const router = useRouter();
4659

47-
const activeTab = (searchParams.get('toolbar-panel') ?? undefined) as
48-
| ToolbarTabValue
49-
| undefined;
50-
51-
const toggled = activeTab !== undefined;
60+
const { activeTab, toggled } = useToolbarState();
5261

5362
const setActivePanelValue = (newValue: ToolbarTabValue | undefined) => {
5463
const params = new URLSearchParams(searchParams);
@@ -115,7 +124,7 @@ const ToolbarInner = ({
115124
className={cn(
116125
'absolute bottom-0 left-0 right-0',
117126
'bg-black group/toolbar text-xs text-slate-11 h-52 transition-transform',
118-
'data-[toggled=false]:translate-y-[170px]',
127+
'data-[toggled=false]:translate-y-[10.625rem]',
119128
)}
120129
>
121130
<Tabs.Root

0 commit comments

Comments
 (0)