Skip to content

Commit d2344ea

Browse files
Example site performance improvements and optimization (#5953)
* refactor: implement dynamic imports for example components to optimize bundle size * feat: use next/font and preconnect to increase page load speed * refactor: move head component to _document to remove warning from console. * fix: add lang attribute to Html tag for improved accessibility * refactor: move common layout to _app and ExampleLayout * chore(deps): upgrade @faker-js/faker from 8.2.0 to 10.0.0 (supports tree-shaking) * refactor: simplify ExampleLayout by replacing inline styles with CSS classes * feat: add loading components for dynamic imports and enhance loading styles * feat: add meta description to improve SEO * refactor: extract example names and paths to a constants file and simplify dynamic imports in ExamplePage
1 parent 70e8819 commit d2344ea

File tree

9 files changed

+602
-426
lines changed

9 files changed

+602
-426
lines changed

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@
5656
"@changesets/changelog-github": "^0.4.8",
5757
"@changesets/cli": "^2.26.2",
5858
"@emotion/css": "^11.11.2",
59-
"@faker-js/faker": "^8.2.0",
59+
"@faker-js/faker": "^10.0.0",
6060
"@playwright/test": "^1.52.0",
6161
"@types/is-hotkey": "^0.1.10",
6262
"@types/is-url": "^1.2.32",
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 function ComponentLoader() {
4+
return (
5+
<div className="loading-container loading-spinner">
6+
<div className="spinner" />
7+
<p className="loading-text">Loading example...</p>
8+
</div>
9+
)
10+
}
11+
12+
export function HugeDocumentLoader() {
13+
return (
14+
<div className="loading-container huge-loader-container">
15+
<div className="spinner" />
16+
<h2 className="loading-text huge-title">Loading Huge Document</h2>
17+
<p className="loading-text huge-subtitle">
18+
Preparing thousands of nodes...
19+
</p>
20+
</div>
21+
)
22+
}

site/components/ExampleLayout.tsx

Lines changed: 205 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,205 @@
1+
import React, { useState, ErrorInfo } from 'react'
2+
import Link from 'next/link'
3+
import { Icon } from '../examples/ts/components/index'
4+
import { NON_HIDDEN_EXAMPLES } from '../constants/examples'
5+
6+
const Header = (props: React.HTMLAttributes<HTMLDivElement>) => (
7+
<div {...props} className="example-header" />
8+
)
9+
10+
const Title = (props: React.HTMLAttributes<HTMLSpanElement>) => (
11+
<span {...props} className="example-title" />
12+
)
13+
14+
const LinkList = (props: React.HTMLAttributes<HTMLDivElement>) => (
15+
<div {...props} className="example-link-list" />
16+
)
17+
18+
const A = (props: React.AnchorHTMLAttributes<HTMLAnchorElement>) => (
19+
<a {...props} className="example-link" />
20+
)
21+
22+
const Pill = (props: React.HTMLAttributes<HTMLSpanElement>) => (
23+
<span {...props} className="example-pill" />
24+
)
25+
26+
const TabList = ({
27+
isVisible,
28+
...props
29+
}: React.HTMLAttributes<HTMLDivElement> & { isVisible?: boolean }) => (
30+
<div
31+
role="menu"
32+
aria-label="Examples navigation"
33+
aria-hidden={!isVisible}
34+
{...props}
35+
className={`example-tab-list ${isVisible ? 'visible' : 'hidden'}`}
36+
/>
37+
)
38+
39+
const TabListUnderlay = ({
40+
isVisible,
41+
...props
42+
}: React.HTMLAttributes<HTMLDivElement> & { isVisible?: boolean }) => (
43+
<div
44+
{...props}
45+
className={`example-tab-list-underlay ${isVisible ? 'visible' : 'hidden'}`}
46+
/>
47+
)
48+
49+
const TabButton = (props: React.HTMLAttributes<HTMLSpanElement>) => (
50+
<button
51+
{...props}
52+
aria-label="Toggle examples menu"
53+
aria-haspopup="menu"
54+
className="example-tab-button"
55+
/>
56+
)
57+
58+
const Tab = React.forwardRef(
59+
(
60+
{
61+
active,
62+
href,
63+
...props
64+
}: React.AnchorHTMLAttributes<HTMLAnchorElement> & {
65+
active: boolean
66+
},
67+
ref: React.Ref<HTMLAnchorElement>
68+
) => (
69+
<a
70+
ref={ref}
71+
href={href}
72+
role="menuitem"
73+
aria-current={active ? 'page' : undefined}
74+
{...props}
75+
className={`example-tab ${active ? 'active' : ''}`}
76+
/>
77+
)
78+
)
79+
80+
const ExampleHeader = (props: React.HTMLAttributes<HTMLDivElement>) => (
81+
<div {...props} className="example-page-header" />
82+
)
83+
84+
const ExampleTitle = (props: React.HTMLAttributes<HTMLSpanElement>) => (
85+
<span {...props} className="example-page-title" />
86+
)
87+
88+
const ExampleContent = (props: React.HTMLAttributes<HTMLDivElement>) => (
89+
<div {...props} className="example-content" />
90+
)
91+
92+
export const Warning = (props: React.HTMLAttributes<HTMLDivElement>) => (
93+
<div {...props} className="example-warning" />
94+
)
95+
96+
interface ExampleLayoutProps {
97+
children: React.ReactNode
98+
exampleName?: string
99+
examplePath?: string
100+
error?: Error | null
101+
stackTrace?: ErrorInfo | null
102+
}
103+
104+
export function ExampleLayout({
105+
children,
106+
exampleName,
107+
examplePath,
108+
error,
109+
stackTrace,
110+
}: ExampleLayoutProps) {
111+
const [showTabs, setShowTabs] = useState<boolean>(false)
112+
113+
return (
114+
<div>
115+
<Header>
116+
<Title>Slate Examples</Title>
117+
<LinkList>
118+
<A href="https://github.com/ianstormtaylor/slate">GitHub</A>
119+
<A href="https://docs.slatejs.org/">Docs</A>
120+
</LinkList>
121+
</Header>
122+
123+
{exampleName && examplePath && (
124+
<ExampleHeader>
125+
<TabButton
126+
onClick={e => {
127+
e.stopPropagation()
128+
setShowTabs(!showTabs)
129+
}}
130+
onKeyDown={(e: React.KeyboardEvent) => {
131+
if (e.key === 'Escape') {
132+
setShowTabs(false)
133+
}
134+
}}
135+
aria-expanded={showTabs}
136+
>
137+
<Icon>menu</Icon>
138+
</TabButton>
139+
<ExampleTitle>
140+
{exampleName}
141+
<A
142+
href={`https://github.com/ianstormtaylor/slate/blob/main/site/examples/js/${examplePath}.jsx`}
143+
>
144+
<Pill>JS Code</Pill>
145+
</A>
146+
<A
147+
href={`https://github.com/ianstormtaylor/slate/blob/main/site/examples/ts/${examplePath}.tsx`}
148+
>
149+
<Pill>TS Code</Pill>
150+
</A>
151+
</ExampleTitle>
152+
</ExampleHeader>
153+
)}
154+
155+
<TabList isVisible={showTabs}>
156+
{NON_HIDDEN_EXAMPLES.map(([n, p]) => (
157+
<Link
158+
key={p as string}
159+
href="/examples/[example]"
160+
as={`/examples/${p}`}
161+
legacyBehavior
162+
passHref
163+
>
164+
<Tab
165+
onClick={() => setShowTabs(false)}
166+
active={p === examplePath}
167+
onKeyDown={(e: React.KeyboardEvent) => {
168+
if (e.key === 'Escape') {
169+
setShowTabs(false)
170+
}
171+
}}
172+
>
173+
{n}
174+
</Tab>
175+
</Link>
176+
))}
177+
</TabList>
178+
179+
{error && stackTrace ? (
180+
<Warning>
181+
<p>An error was thrown by one of the example's React components!</p>
182+
<pre>
183+
<code>
184+
{error.stack}
185+
{'\n'}
186+
{stackTrace.componentStack}
187+
</code>
188+
</pre>
189+
</Warning>
190+
) : (
191+
<ExampleContent>{children}</ExampleContent>
192+
)}
193+
194+
<TabListUnderlay
195+
isVisible={showTabs}
196+
onClick={() => setShowTabs(false)}
197+
onKeyDown={(e: React.KeyboardEvent) => {
198+
if (e.key === 'Escape') {
199+
setShowTabs(false)
200+
}
201+
}}
202+
/>
203+
</div>
204+
)
205+
}

site/constants/examples.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
export const EXAMPLE_NAMES_AND_PATHS = [
2+
['Android Tests', 'android-tests'],
3+
['Checklists', 'check-lists'],
4+
['Code Highlighting', 'code-highlighting'],
5+
['Custom Placeholder', 'custom-placeholder'],
6+
['Editable Voids', 'editable-voids'],
7+
['Embeds', 'embeds'],
8+
['Forced Layout', 'forced-layout'],
9+
['Hovering Toolbar', 'hovering-toolbar'],
10+
['Huge Document', 'huge-document'],
11+
['Images', 'images'],
12+
['Inlines', 'inlines'],
13+
['Markdown Preview', 'markdown-preview'],
14+
['Markdown Shortcuts', 'markdown-shortcuts'],
15+
['Mentions', 'mentions'],
16+
['Paste HTML', 'paste-html'],
17+
['Plain Text', 'plaintext'],
18+
['Read-only', 'read-only'],
19+
['Rendering in iframes', 'iframe'],
20+
['Rich Text', 'richtext'],
21+
['Search Highlighting', 'search-highlighting'],
22+
['Shadow DOM', 'shadow-dom'],
23+
['Styling', 'styling'],
24+
['Tables', 'tables'],
25+
] as const
26+
27+
export const HIDDEN_EXAMPLES = ['android-tests'] as const
28+
29+
export const NON_HIDDEN_EXAMPLES = EXAMPLE_NAMES_AND_PATHS.filter(
30+
([, path]) => !HIDDEN_EXAMPLES.includes(path as any)
31+
)

site/pages/_app.tsx

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import React, { useState, ErrorInfo } from 'react'
2+
import { AppProps } from 'next/app'
3+
import { ErrorBoundary } from 'react-error-boundary'
4+
import { Roboto } from 'next/font/google'
5+
import { ExampleLayout, Warning } from '../components/ExampleLayout'
6+
7+
const roboto = Roboto({
8+
weight: ['400', '700'],
9+
style: ['normal', 'italic'],
10+
subsets: ['latin', 'latin-ext'],
11+
display: 'swap',
12+
})
13+
14+
function ErrorFallback({
15+
error,
16+
resetErrorBoundary,
17+
}: {
18+
error: Error
19+
resetErrorBoundary: () => void
20+
}) {
21+
return (
22+
<Warning>
23+
<p>An error was thrown by one of the example's React components!</p>
24+
<pre>
25+
<code>{error.stack}</code>
26+
</pre>
27+
<button onClick={resetErrorBoundary}>Try again</button>
28+
</Warning>
29+
)
30+
}
31+
32+
export default function App({ Component, pageProps }: AppProps) {
33+
const [error, setError] = useState<Error | undefined>(undefined)
34+
const [stackTrace, setStackTrace] = useState<ErrorInfo | undefined>(undefined)
35+
return (
36+
<div className={roboto.className}>
37+
<ErrorBoundary
38+
FallbackComponent={ErrorFallback}
39+
onError={(error, stackTrace) => {
40+
setError(error)
41+
setStackTrace(stackTrace)
42+
}}
43+
>
44+
<ExampleLayout
45+
exampleName={pageProps.exampleName}
46+
examplePath={pageProps.examplePath}
47+
error={error}
48+
stackTrace={stackTrace}
49+
>
50+
<Component {...pageProps} />
51+
</ExampleLayout>
52+
</ErrorBoundary>
53+
</div>
54+
)
55+
}

site/pages/_document.tsx

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import React from 'react'
2+
import { Html, Head, Main, NextScript } from 'next/document'
3+
4+
export default function Document() {
5+
return (
6+
<Html lang="en">
7+
<Head>
8+
<meta
9+
name="description"
10+
content="Slate is a completely customizable framework for building rich text editors. Learn how to build powerful editors with React and TypeScript."
11+
/>
12+
<link rel="icon" href="/favicon.ico" />
13+
<link rel="stylesheet" href="/index.css" />
14+
<link rel="preconnect" href="https://fonts.googleapis.com" />
15+
<link
16+
rel="preconnect"
17+
href="https://fonts.gstatic.com"
18+
crossOrigin=""
19+
/>
20+
<link
21+
rel="stylesheet"
22+
href="https://fonts.googleapis.com/icon?family=Material+Icons"
23+
/>
24+
</Head>
25+
<body>
26+
<Main />
27+
<NextScript />
28+
</body>
29+
</Html>
30+
)
31+
}

0 commit comments

Comments
 (0)