Skip to content

Commit

Permalink
feat: add interactive demo component to the new website (#7884)
Browse files Browse the repository at this point in the history
  • Loading branch information
tkajtoch authored Jul 18, 2024
1 parent 8e9df72 commit 2680969
Show file tree
Hide file tree
Showing 20 changed files with 544 additions and 20 deletions.
9 changes: 6 additions & 3 deletions packages/docusaurus-theme/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,7 @@
},
"devDependencies": {
"@docusaurus/types": "^3.4.0",
"@types/react": "^18",
"@types/react-dom": "^18",
"@types/react-is": "^18",
"typescript": "~5.4.5"
},
"main": "lib/index.js",
Expand All @@ -36,7 +35,11 @@
"@elastic/eui": "94.5.0",
"@emotion/css": "^11.11.2",
"@emotion/react": "^11.11.4",
"moment": "^2.30.1"
"clsx": "^2.1.1",
"moment": "^2.30.1",
"prism-react-renderer": "^2.3.1",
"react-is": "^18.3.1",
"react-live": "^4.1.7"
},
"peerDependencies": {
"react": "^18.0.0",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export const CodeSandboxIcon = () => (
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" preserveAspectRatio="xMidYMid"
viewBox="-20 0 296 296">
<path
d="M115.498 261.088v-106.61L23.814 101.73v60.773l41.996 24.347v45.7l49.688 28.54Zm23.814.627 50.605-29.151V185.78l42.269-24.495v-60.011l-92.874 53.621v106.82Zm80.66-180.887-48.817-28.289-42.863 24.872-43.188-24.897-49.252 28.667 91.914 52.882 92.206-53.235ZM0 222.212V74.495L127.987 0 256 74.182v147.797l-128.016 73.744L0 222.212Z" />
</svg>
);
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { CodeSandboxIcon } from './codesandbox_icon';
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import {
EuiButton,
EuiButtonIcon,
EuiFlexGroup,
EuiToolTip,
useEuiMemoizedStyles,
UseEuiTheme,
darken,
} from '@elastic/eui';
import { css } from '@emotion/react';
import { CodeSandboxIcon } from '../../codesandbox_icon';

export interface DemoActionsBarProps {
isSourceOpen: boolean;
setSourceOpen(isOpen: boolean): void;
onClickReloadExample(): void;
onClickCopyToClipboard(): void;
onClickOpenInCodeSandbox(): void;
}

const getDemoActionsBarStyles = (euiTheme: UseEuiTheme) => {
return {
actionsBar: css`
padding: var(--eui-size-s);
background: ${darken(euiTheme.euiTheme.colors.body, 0.05)};
border-top: 1px solid var(--docs-demo-border-color);
&:last-child {
// border radius should be 1px smaller to work nicely
// with the wrapper border width of 1px
border-radius: 0 0 calc(var(--docs-demo-border-radius) - 1px) calc(var(--docs-demo-border-radius) - 1px);
}
`,
button: css`
background: var(--eui-background-color-primary-opaque);
border: 1px solid var(--eui-border-color-primary);
margin-right: auto;
`,
};
}

export const DemoActionsBar = ({
isSourceOpen,
setSourceOpen,
onClickOpenInCodeSandbox,
onClickReloadExample,
onClickCopyToClipboard
}: DemoActionsBarProps) => {
const styles = useEuiMemoizedStyles(getDemoActionsBarStyles);

return (
<EuiFlexGroup alignItems="center" css={styles.actionsBar} gutterSize="s">
<EuiButton
css={styles.button}
onClick={() => setSourceOpen(!isSourceOpen)}
size="s"
minWidth={false}
>
{isSourceOpen ? 'Hide source' : 'Show source'}
</EuiButton>
<EuiToolTip content="Open in CodeSandbox">
<EuiButtonIcon
size="s"
iconType={CodeSandboxIcon}
color="text"
aria-label="Open in CodeSandbox"
onClick={onClickOpenInCodeSandbox}
/>
</EuiToolTip>
<EuiToolTip content="Copy to clipboard">
<EuiButtonIcon
size="s"
iconType="copyClipboard"
color="text"
onClick={onClickCopyToClipboard}
aria-label="Copy code to clipboard"
/>
</EuiToolTip>
<EuiToolTip content="Reload example">
<EuiButtonIcon
size="s"
iconType="refresh"
color="text"
onClick={onClickReloadExample}
aria-label="Reload example"
/>
</EuiToolTip>
</EuiFlexGroup>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { DemoActionsBar, type DemoActionsBarProps } from './actions_bar';
14 changes: 14 additions & 0 deletions packages/docusaurus-theme/src/components/demo/context.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { createContext, useContext } from 'react';
import { DemoSourceMeta } from './demo';

export interface DemoContextObject {
sources: DemoSourceMeta[];
addSource(source: DemoSourceMeta): void;
}

export const DemoContext = createContext<DemoContextObject>({
sources: [],
addSource: () => {},
});

export const useDemoContext = () => useContext(DemoContext);
97 changes: 97 additions & 0 deletions packages/docusaurus-theme/src/components/demo/demo.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import { Children, PropsWithChildren, useCallback, useState } from 'react';
import { isElement } from 'react-is';
import { LiveProvider } from 'react-live';
import { themes as prismThemes } from 'prism-react-renderer';
import { useEuiMemoizedStyles, copyToClipboard, UseEuiTheme } from '@elastic/eui';
import { css } from '@emotion/react';
import { DemoContext, DemoContextObject } from './context';
import { DemoEditor } from './editor';
import { DemoPreview } from './preview';
import { DemoSource } from './source';
import { demoScope } from './scope';
import { DemoActionsBar } from './actions_bar';

export interface DemoSourceMeta {
code: string;
isActive: boolean;
filename?: string;
}

export interface DemoProps extends PropsWithChildren {
isSourceOpen?: boolean;
}

const transformCode = (code: string) => code;

const getDemoStyles = (euiTheme: UseEuiTheme) => ({
demo: css`
--docs-demo-border-color: ${euiTheme.euiTheme.colors.lightShade};
--docs-demo-border-radius: ${euiTheme.euiTheme.size.s};
border: 1px solid var(--docs-demo-border-color);
border-radius: var(--docs-demo-border-radius);
margin-block: ${euiTheme.euiTheme.size.xl};
`,
});

export const Demo = ({
children,
isSourceOpen: _isSourceOpen = true
}: DemoProps) => {
const styles = useEuiMemoizedStyles(getDemoStyles);
const [sources, setSources] = useState<DemoSourceMeta[]>([]);
const [isSourceOpen, setIsSourceOpen] = useState<boolean>(_isSourceOpen);
const activeSource = sources[0];

// liveProviderKey restarts the demo to its initial state
const [liveProviderKey, setLiveProviderKey] = useState<number>(0);

const addSource = useCallback<DemoContextObject['addSource']>((source: DemoSourceMeta) => {
setSources((sources) => ([...sources, source]));
}, []);

const onClickCopyToClipboard = useCallback(() => {
copyToClipboard(activeSource?.code || '');
}, [activeSource]);

const onClickReloadExample = useCallback(() => {
setLiveProviderKey((liveProviderKey) => liveProviderKey + 1);
}, []);

const onClickOpenInCodeSandbox = useCallback(() => {
// TODO: implement
console.error('Open in CodeSandbox action is not implemented yet');
}, []);

return (
<div css={styles.demo}>
<DemoContext.Provider value={{ sources, addSource }}>
<LiveProvider
key={liveProviderKey}
code={activeSource?.code || ''}
transformCode={transformCode}
theme={prismThemes.dracula}
scope={demoScope}
>
<DemoPreview />
<DemoActionsBar
isSourceOpen={isSourceOpen}
setSourceOpen={setIsSourceOpen}
onClickCopyToClipboard={onClickCopyToClipboard}
onClickReloadExample={onClickReloadExample}
onClickOpenInCodeSandbox={onClickOpenInCodeSandbox}
/>
{isSourceOpen && <DemoEditor />}
</LiveProvider>

{Children.map(children, (child, index) => {
if (isElement(child) && child.type === DemoSource) {
return child;
}

return <DemoSource isActive={index === 0}>{child}</DemoSource>;
})}
</DemoContext.Provider>
</div>
);
};
49 changes: 49 additions & 0 deletions packages/docusaurus-theme/src/components/demo/editor/editor.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/

import { LiveEditor, LiveError } from 'react-live';
import { css } from '@emotion/react';
import { useEuiMemoizedStyles, useEuiTheme } from '@elastic/eui';

const getEditorStyles = () => ({
editor: css`
font-family: var(--ifm-font-family-monospace);
border-radius: 0 0 var(--docs-demo-border-radius) var(--docs-demo-border-radius);
& .prism-code {
border-radius: 0 0 calc(var(--docs-demo-border-radius) - 1px) calc(var(--docs-demo-border-radius) - 1px);
}
`,
error: css`
// docusaurus overrides the default pre styles
// forcing us to use higher specificity here
&& > pre {
font-size: var(--eui-font-size-s);
background: var(--eui-background-color-danger);
color: var(--eui-color-danger-text);
padding: var(--eui-size-xs) var(--eui-size-s);
margin: 0;
border-radius: 0;
}
`,
});

export const DemoEditor = () => {
const styles = useEuiMemoizedStyles(getEditorStyles);

return (
<div css={styles.editor}>
<div css={styles.error}>
<LiveError />
</div>
<div>
<LiveEditor tabMode="focus" />
</div>
</div>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { DemoEditor } from './editor';
2 changes: 2 additions & 0 deletions packages/docusaurus-theme/src/components/demo/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export { Demo, type DemoProps } from './demo';
export { DemoSource } from './source';
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { DemoPreview } from './preview';
41 changes: 41 additions & 0 deletions packages/docusaurus-theme/src/components/demo/preview/preview.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { css } from '@emotion/react';
import { LiveError, LivePreview } from 'react-live';
import BrowserOnly from '@docusaurus/BrowserOnly';
import ErrorBoundary from '@docusaurus/ErrorBoundary';
import { ErrorBoundaryErrorMessageFallback } from '@docusaurus/theme-common';
import { UseEuiTheme, EuiFlexGroup, useEuiTheme } from '@elastic/eui';

const getPreviewStyles = (euiTheme: UseEuiTheme) => ({
previewWrapper: css`
padding: ${euiTheme.euiTheme.size.l};
border-radius: var(--docs-demo-border-radius);
`,
});

/**
* PreviewLoader provides a fallback content for the server-side render
* of the live component preview component.
* Due to the limitations of react-live the demo is only rendered client-side.
*/
const PreviewLoader = () => (
<div>Loading...</div>
);

export const DemoPreview = () => {
const euiTheme = useEuiTheme();
const styles = getPreviewStyles(euiTheme);

return (
<BrowserOnly fallback={<PreviewLoader />}>
{() => (
<>
<ErrorBoundary fallback={(params: any) => <ErrorBoundaryErrorMessageFallback {...params} />}>
<EuiFlexGroup css={styles.previewWrapper} alignItems="center" justifyContent="center">
<LivePreview />
</EuiFlexGroup>
</ErrorBoundary>
</>
)}
</BrowserOnly>
);
};
8 changes: 8 additions & 0 deletions packages/docusaurus-theme/src/components/demo/scope.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import React from 'react';
import * as EUI from '@elastic/eui';

export const demoScope: Record<string, unknown> = {
React,
...React,
...EUI,
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { Children, ReactElement, ReactNode } from 'react';
import { isElement } from 'react-is';

export interface SourceMeta {
/**
* The source code
*/
code: string;
}

/**
* Get source string from given children.
*/
export const getSourceFromChildren = (children: ReactNode): string | null => {
if (Children.count(children) !== 1 || !isElement(children)) {
// This should never happen
return null;
}

const element = children as ReactElement;
const functionName = (element.type as Function).name;
// The code block content could render in either MDXPre (development builds)
// or pre (optimized production builds)
if (
typeof element.type !== 'function' ||
(functionName !== 'MDXPre' && functionName !== 'pre')
) {
return null;
}

if (!isElement(element.props.children)) {
return null;
}

const codeElement = element.props.children as ReactElement;
if (!codeElement || !codeElement.props.children) {
return null;
}

const code = codeElement.props.children;
if (typeof code !== 'string') {
return null;
}

return code;
};
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { DemoSource, type DemoSourceProps } from './source';
Loading

0 comments on commit 2680969

Please sign in to comment.