Skip to content

Commit bbf7493

Browse files
feat(examples): Add support for live and editable examples (#49)
* feat(examples): Add support for live and editable examples * Move ExampleToolbar into the repo * Add other content component overrides
1 parent 7b935f6 commit bbf7493

16 files changed

+2517
-231
lines changed

astro.config.mjs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
// @ts-check
22
import { defineConfig } from 'astro/config';
33
import react from '@astrojs/react';
4+
import mdx from '@astrojs/mdx';
45

56
import node from '@astrojs/node';
67

78
// https://astro.build/config
89
export default defineConfig({
9-
integrations: [react()],
10-
10+
integrations: [react(), mdx()],
1111
vite: {
1212
ssr: {
1313
noExternal: ["@patternfly/*", "react-dropzone"],

declarations.d.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
declare module '@patternfly/documentation-framework/components/example/exampleToolbar'
2+
declare module '@patternfly/ast-helpers'

package-lock.json

Lines changed: 1352 additions & 188 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,16 +42,19 @@
4242
"dependencies": {
4343
"@astrojs/check": "^0.9.4",
4444
"@astrojs/node": "^9.1.3",
45+
"@astrojs/mdx": "^4.2.6",
4546
"@astrojs/react": "^4.2.1",
4647
"@nanostores/react": "^0.8.0",
48+
"@patternfly/ast-helpers": "^1.4.0-alpha.190",
4749
"@patternfly/patternfly": "^6.0.0",
50+
"@patternfly/react-code-editor": "^6.2.2",
4851
"@patternfly/react-core": "^6.0.0",
4952
"@patternfly/react-table": "^6.0.0",
50-
"commander": "^13.1.0",
5153
"@types/react": "^18.3.12",
5254
"@types/react-dom": "^18.3.1",
5355
"astro": "^5.4.1",
5456
"change-case": "5.4.4",
57+
"commander": "^13.1.0",
5558
"glob": "^11.0.1",
5659
"nanostores": "^0.11.3",
5760
"react": "^18.3.1",

src/components/Content.tsx

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import { Content as PFContent } from '@patternfly/react-core'
2+
import { ReactNode } from 'react'
3+
4+
export const h1 = ({ children }: { children: ReactNode }) => (
5+
<PFContent component="h1">{children}</PFContent>
6+
)
7+
export const h2 = ({ children }: { children: ReactNode }) => (
8+
<PFContent component="h2">{children}</PFContent>
9+
)
10+
export const h3 = ({ children }: { children: ReactNode }) => (
11+
<PFContent component="h3">{children}</PFContent>
12+
)
13+
export const h4 = ({ children }: { children: ReactNode }) => (
14+
<PFContent component="h4">{children}</PFContent>
15+
)
16+
export const h5 = ({ children }: { children: ReactNode }) => (
17+
<PFContent component="h5">{children}</PFContent>
18+
)
19+
export const h6 = ({ children }: { children: ReactNode }) => (
20+
<PFContent component="h6">{children}</PFContent>
21+
)
22+
23+
export const p = ({ children }: { children: ReactNode }) => (
24+
<PFContent component="p">{children}</PFContent>
25+
)
26+
export const a = ({ children }: { children: ReactNode }) => (
27+
<PFContent component="a">{children}</PFContent>
28+
)
29+
export const small = ({ children }: { children: ReactNode }) => (
30+
<PFContent component="small">{children}</PFContent>
31+
)
32+
33+
export const blockquote = ({ children }: { children: ReactNode }) => (
34+
<PFContent component="blockquote">{children}</PFContent>
35+
)
36+
export const pre = ({ children }: { children: ReactNode }) => (
37+
<PFContent component="pre">{children}</PFContent>
38+
)
39+
export const hr = ({ children }: { children: ReactNode }) => (
40+
<PFContent component="hr">{children}</PFContent>
41+
)
42+
43+
export const ul = ({ children }: { children: ReactNode }) => (
44+
<PFContent component="ul">{children}</PFContent>
45+
)
46+
export const ol = ({ children }: { children: ReactNode }) => (
47+
<PFContent component="ol">{children}</PFContent>
48+
)
49+
export const dl = ({ children }: { children: ReactNode }) => (
50+
<PFContent component="dl">{children}</PFContent>
51+
)
52+
export const li = ({ children }: { children: ReactNode }) => (
53+
<PFContent component="li">{children}</PFContent>
54+
)
55+
56+
export const dt = ({ children }: { children: ReactNode }) => (
57+
<PFContent component="dt">{children}</PFContent>
58+
)
59+
export const dd = ({ children }: { children: ReactNode }) => (
60+
<PFContent component="dd">{children}</PFContent>
61+
)

src/components/ExampleToolbar.tsx

Lines changed: 296 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,296 @@
1+
// TODO remove this component it is not good and I don't like that I have to add it
2+
3+
import React from 'react'
4+
import { Button, Form, Tooltip } from '@patternfly/react-core'
5+
import {
6+
CodeEditor,
7+
CodeEditorControl,
8+
Language,
9+
} from '@patternfly/react-code-editor'
10+
import { convertToJSX } from '@patternfly/ast-helpers'
11+
import ExternalLinkAltIcon from '@patternfly/react-icons/dist/esm/icons/external-link-alt-icon'
12+
import CodepenIcon from '@patternfly/react-icons/dist/esm/icons/codepen-icon'
13+
import CopyIcon from '@patternfly/react-icons/dist/esm/icons/copy-icon'
14+
import CodeIcon from '@patternfly/react-icons/dist/esm/icons/code-icon'
15+
import AngleDoubleRightIcon from '@patternfly/react-icons/dist/esm/icons/angle-double-right-icon'
16+
import ReplyAllIcon from '@patternfly/react-icons/dist/esm/icons/reply-all-icon'
17+
18+
const copy = (textToCopy: string) => {
19+
navigator.clipboard.writeText(textToCopy)
20+
}
21+
22+
interface TrackEventValue {
23+
event_category: string
24+
event_label: string
25+
value?: number
26+
}
27+
28+
interface WindowWithGtag extends Window {
29+
gtag?: (type: string, name: string, value: TrackEventValue) => void
30+
}
31+
const _window = window as WindowWithGtag
32+
/**
33+
* Sends network call using Google Analytic's gtag function
34+
* https://developers.google.com/analytics/devguides/collection/gtagjs/events#send_events
35+
*
36+
* @param name {string} - The value that will appear as the event action in Google Analytics Event reports.
37+
* @param eventCategory {string} - The category of the event.
38+
* @param eventLabel {string} - The label of the event.
39+
* @param [value] {number} - An optional non-negative integer that will appear as the event value.
40+
*/
41+
const trackEvent = (
42+
name: string,
43+
eventCategory: string,
44+
eventLabel: string,
45+
value?: number,
46+
) => {
47+
value ||= 0
48+
if (_window?.gtag) {
49+
_window?.gtag('event', name, {
50+
// eslint-disable-next-line camelcase
51+
event_category: eventCategory,
52+
// eslint-disable-next-line camelcase
53+
event_label: eventLabel,
54+
// Optional non-negative integer
55+
...(value >= 0 && { value }),
56+
})
57+
}
58+
}
59+
60+
function getLanguage(lang: string) {
61+
if (lang === 'js') {
62+
return Language.javascript
63+
} else if (lang === 'ts') {
64+
return Language.typescript
65+
}
66+
67+
return lang as Language
68+
}
69+
70+
interface ExampleToolbarProps {
71+
fullscreenLink?: string
72+
codeBoxParams?: any
73+
lang: string
74+
isFullscreen?: boolean
75+
originalCode: string
76+
code: string
77+
setCode: (code: string) => void
78+
exampleTitle?: string
79+
}
80+
81+
export const ExampleToolbar = ({
82+
// Link to fullscreen example page (each example has one)
83+
fullscreenLink,
84+
// Params to pass to codesandbox
85+
codeBoxParams,
86+
// Language of code
87+
lang,
88+
// Whether the example is fullscreen only
89+
isFullscreen,
90+
// Original version of the code
91+
originalCode,
92+
// Current code in editor
93+
code,
94+
// Callback to set code in parent component
95+
setCode,
96+
// Title of example used in creating unique labels
97+
exampleTitle,
98+
}: ExampleToolbarProps) => {
99+
const [isEditorOpen, setIsEditorOpen] = React.useState(false)
100+
const [isCopied, setCopied] = React.useState(false)
101+
102+
const copyLabel = 'Copy code to clipboard'
103+
const languageLabel = `Toggle ${lang.toUpperCase()} code`
104+
const codesandboxLabel = 'Open example in CodeSandbox'
105+
const fullscreenLabel = 'Open example in new window'
106+
const convertLabel = 'Convert example from Typescript to JavaScript'
107+
const undoAllLabel = 'Undo all changes'
108+
109+
const copyAriaLabel = `Copy ${exampleTitle} example code to clipboard`
110+
const languageAriaLabel = `Toggle ${lang.toUpperCase()} code in ${exampleTitle} example`
111+
const codesandboxAriaLabel = `Open ${exampleTitle} example in CodeSandbox`
112+
const fullscreenAriaLabel = `Open ${exampleTitle} example in new window`
113+
const convertAriaLabel = `Convert ${exampleTitle} example from Typescript to JavaScript`
114+
const undoAllAriaLabel = `Undo all changes to ${exampleTitle}`
115+
116+
const editorControlProps = {
117+
className: 'ws-code-editor-control',
118+
}
119+
120+
const commonTooltipProps = {
121+
exitDelay: 300,
122+
maxWidth: 'var(--ws-code-editor--tooltip--MaxWidth)',
123+
}
124+
125+
const copyCode = () => {
126+
copy(code)
127+
setCopied(true)
128+
}
129+
130+
const customControls = (
131+
<React.Fragment>
132+
<CodeEditorControl
133+
icon={
134+
<React.Fragment>
135+
<CodeIcon />
136+
{' ' + lang.toUpperCase()}
137+
</React.Fragment>
138+
}
139+
onClick={() => {
140+
setIsEditorOpen(!isEditorOpen)
141+
// 1 === expand code, 0 === collapse code
142+
trackEvent(
143+
'code_editor_control_click',
144+
'click_event',
145+
'TOGGLE_CODE',
146+
isEditorOpen ? 0 : 1,
147+
)
148+
}}
149+
tooltipProps={{
150+
content: languageLabel,
151+
...commonTooltipProps,
152+
}}
153+
aria-label={languageAriaLabel}
154+
aria-expanded={isEditorOpen}
155+
{...editorControlProps}
156+
/>
157+
<Tooltip
158+
content={<div>{isCopied ? 'Code copied' : copyLabel}</div>}
159+
maxWidth={(editorControlProps as any)?.maxWidth}
160+
exitDelay={isCopied ? 300 : 1600}
161+
onTooltipHidden={() => setCopied(false)}
162+
>
163+
<Button
164+
onClick={() => {
165+
copyCode()
166+
trackEvent('code_editor_control_click', 'click_event', 'COPY_CODE')
167+
}}
168+
variant="plain"
169+
aria-label={copyAriaLabel}
170+
className={editorControlProps.className}
171+
>
172+
<CopyIcon />
173+
</Button>
174+
</Tooltip>
175+
{codeBoxParams && (
176+
<Form
177+
aria-label={`${codesandboxAriaLabel} form`}
178+
action="https://codesandbox.io/api/v1/sandboxes/define"
179+
method="POST"
180+
target="_blank"
181+
style={{ display: 'inline-block' }}
182+
>
183+
<Tooltip
184+
content={codesandboxLabel}
185+
maxWidth={(editorControlProps as any).maxWidth}
186+
>
187+
<Button
188+
aria-label={codesandboxAriaLabel}
189+
variant="plain"
190+
type="submit"
191+
onClick={() => {
192+
trackEvent(
193+
'code_editor_control_click',
194+
'click_event',
195+
'CODESANDBOX_LINK',
196+
)
197+
}}
198+
className={editorControlProps.className}
199+
>
200+
<CodepenIcon />
201+
</Button>
202+
</Tooltip>
203+
<input type="hidden" name="parameters" value={codeBoxParams} />
204+
</Form>
205+
)}
206+
{fullscreenLink && (
207+
<CodeEditorControl
208+
component="a"
209+
icon={<ExternalLinkAltIcon />}
210+
href={fullscreenLink}
211+
target="_blank"
212+
rel="noopener noreferrer"
213+
aria-label={fullscreenAriaLabel}
214+
tooltipProps={{
215+
content: fullscreenLabel,
216+
...commonTooltipProps,
217+
}}
218+
onClick={() => {
219+
trackEvent(
220+
'code_editor_control_click',
221+
'click_event',
222+
'FULLSCREEN_LINK',
223+
)
224+
}}
225+
{...editorControlProps}
226+
/>
227+
)}
228+
{isEditorOpen && lang === 'ts' && (
229+
<CodeEditorControl
230+
icon={
231+
<React.Fragment>
232+
{'TS '}
233+
<AngleDoubleRightIcon />
234+
{' JS'}
235+
</React.Fragment>
236+
}
237+
aria-label={convertAriaLabel}
238+
tooltipProps={{
239+
content: convertLabel,
240+
...commonTooltipProps,
241+
}}
242+
onClick={() => {
243+
setCode(convertToJSX(code).code)
244+
trackEvent('code_editor_control_click', 'click_event', 'TS_TO_JS')
245+
}}
246+
{...editorControlProps}
247+
/>
248+
)}
249+
{code !== originalCode && (
250+
<CodeEditorControl
251+
icon={<ReplyAllIcon />}
252+
aria-label={undoAllAriaLabel}
253+
tooltipProps={{
254+
content: undoAllLabel,
255+
...commonTooltipProps,
256+
}}
257+
onClick={() => {
258+
setCode(originalCode)
259+
trackEvent('code_editor_control_click', 'click_event', 'RESET_CODE')
260+
}}
261+
{...editorControlProps}
262+
/>
263+
)}
264+
</React.Fragment>
265+
)
266+
267+
// TODO: check if worth adding react, patternfly, and example types
268+
// https://microsoft.github.io/monaco-editor/api/interfaces/monaco.languages.typescript.languageservicedefaults.html#addextralib
269+
const onEditorDidMount = (_editor: any, monaco: any) => {
270+
monaco.languages.typescript.typescriptDefaults.setCompilerOptions({
271+
jsx: true,
272+
...monaco.languages.typescript.typescriptDefaults.getCompilerOptions(),
273+
})
274+
monaco.languages.typescript.typescriptDefaults.setDiagnosticsOptions({
275+
noSemanticValidation: true,
276+
noSyntaxValidation: true,
277+
noSuggestionDiagnostics: true,
278+
onlyVisible: true,
279+
})
280+
}
281+
282+
return (
283+
<CodeEditor
284+
customControls={customControls}
285+
showEditor={isEditorOpen}
286+
language={getLanguage(lang)}
287+
height="400px"
288+
code={code}
289+
onChange={(newCode) => setCode(newCode)}
290+
onEditorDidMount={onEditorDidMount}
291+
isReadOnly={isFullscreen}
292+
className={`${isEditorOpen ? 'ws-example-code-expanded ' : ''}ws-code-editor`}
293+
isHeaderPlain
294+
/>
295+
)
296+
}

0 commit comments

Comments
 (0)