Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Component preview #163

Merged
merged 7 commits into from
Aug 8, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,9 @@ slug: /button
## Example

```tsx live
<PreviewBlock>
<PreviewBlock componentName="Button">
<Button variant="primary" size="md">
Test button
Button
</Button>
</PreviewBlock>
```
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,7 @@ slug: /link

## Example

```tsx live
<PreviewBlock>
<Link>Test link</Link>
</PreviewBlock>
```
TODO:

## Props

Expand Down
1 change: 1 addition & 0 deletions @stellar/design-system-website/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
"prism-react-renderer": "^1.3.5",
"react": "^17.0.2",
"react-dom": "^17.0.2",
"react-element-to-jsx-string": "^15.0.0",
"rehype-parse": "^8.0.4",
"rehype-react": "^7.2.0",
"rehype-sanitize": "^5.0.1",
Expand Down
101 changes: 101 additions & 0 deletions @stellar/design-system-website/src/componentPreview/ButtonPreview.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
import React from "react";
import { ComponentPreview } from "@site/src/components/PreviewBlock";
import CheckIconSvg from "@site/static/img/check-icon.svg";

export const ButtonPreview: ComponentPreview = {
options: [
{
type: "select",
prop: "variant",
options: [
{
value: "primary",
label: "Primary",
},
{
value: "secondary",
label: "Secondary",
},
{
value: "tertiary",
label: "Tertiary",
},
{
value: "destructive",
label: "Destructive",
},
{
value: "error",
label: "Error",
},
{
value: "success",
label: "Success",
},
],
},
{
type: "select",
prop: "size",
options: [
{
value: "md",
label: "MD",
},
{
value: "sm",
label: "SM",
},
{
value: "xs",
label: "XS",
},
],
},
{
type: "checkbox",
prop: "isExtraPadding",
label: "Extra padding",
},
{
type: "checkbox",
prop: "isFullWidth",
label: "Full width",
},
{
type: "checkbox",
prop: "isLoading",
label: "Loading",
},
{
type: "checkbox",
prop: "isPill",
label: "Pill",
},
{
type: "checkbox",
prop: "isUppercase",
label: "Uppercase",
},
{
type: "select",
prop: "icon",
customValue: <CheckIconSvg />,
clearProp: "iconPosition",
options: [
{
value: "",
label: "No icon",
},
{
value: '{"iconPosition": "right"}',
label: "Icon right",
},
{
value: '{"iconPosition": "left"}',
label: "Icon left",
},
],
},
],
};
218 changes: 214 additions & 4 deletions @stellar/design-system-website/src/components/PreviewBlock/index.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,216 @@
import React from "react";
import React, { cloneElement, useEffect, useState } from "react";
import reactElementToJSXString from "react-element-to-jsx-string";
import { PlaygroundEditor } from "@site/src/theme/Playground";
import "./styles.css";

export const PreviewBlock = ({ children }: { children: React.ReactNode }) => {
// TODO: theme switch
return <div className="sds-theme-light">{children}</div>;
// Previews
import { ButtonPreview } from "@site/src/componentPreview/ButtonPreview";

type Theme = "sds-theme-light" | "sds-theme-dark";

// Types
export type PreviewOptionType = "select" | "checkbox";

interface PreviewOptionBase {
type: PreviewOptionType;
prop: string;
clearProp?: string;
customValue?: any;
}

interface PreviewOptionSelect extends PreviewOptionBase {
options: {
value: string;
label: string;
}[];
}

interface PreviewOptionCheckbox extends PreviewOptionBase {
label: string;
}

export type ComponentPreview = {
options: (PreviewOptionSelect | PreviewOptionCheckbox)[];
};

/**
* This file handles the layout and logic for the PreviewBlock to render
* components.
*
* Config file for every component is in /src/componentPreview/ directory. File
* names are [componentName]Preview.
*/
export const PreviewBlock = ({
componentName,
children,
}: {
componentName: string;
children: React.ReactElement;
}) => {
const [sds, setSds] = useState<any>({});
const { Checkbox, Select } = sds;

// Importing SDS here because we need it async for server-side-rendering
useEffect(() => {
const initSds = async () => {
setSds(await import("@stellar/design-system"));
};
initSds();
}, []);

const [theme, setTheme] = useState<Theme>("sds-theme-light");

// All component previews
const previews: { [key: string]: ComponentPreview } = {
Button: ButtonPreview,
};

const compPreview = previews[componentName];

if (!compPreview) {
throw Error(`There is no preview for "${componentName}" component.`);
}

const [props, setProps] = useState({});

const handleSelectChange = (
event: React.ChangeEvent<HTMLSelectElement>,
customValue: any,
clearProp: string,
) => {
const { id, value } = event.target;
const _props = props;

let valObj = {};

// If the value is object, assuming it's setting another prop. For example,
// icon dropdown is setting icon and icon position
try {
valObj = JSON.parse(value);
} catch (error) {
// do nothing
}

if (value) {
setProps({ ..._props, [id]: customValue ?? value, ...valObj });
} else {
delete _props[id];

if (clearProp) {
delete _props[clearProp];
}

setProps({ ..._props });
}
};

const handleCheckboxChange = (
event: React.ChangeEvent<HTMLInputElement>,
customValue: any,
clearProp: string,
) => {
const { id, checked } = event.target;
const _props = props;

if (!checked && clearProp && props[clearProp]) {
delete _props[clearProp];
}

setProps({
..._props,
[id]: checked ? customValue ?? true : false,
});
};

const renderSelect = (option: PreviewOptionSelect) => {
return Select ? (
<Select
id={option.prop}
fieldSize="sm"
onChange={(e) =>
handleSelectChange(e, option.customValue, option.clearProp)
}
key={option.prop}
>
<>
{option.options.map((o) => (
<option value={o.value} key={o.value}>
{o.label}
</option>
))}
</>
</Select>
) : null;
};

const renderCheckbox = (option: PreviewOptionCheckbox) => {
return Checkbox ? (
<Checkbox
id={option.prop}
fieldSize="sm"
label={option.label}
onChange={(e) =>
handleCheckboxChange(e, option.customValue, option.clearProp)
}
key={option.prop}
/>
) : null;
};

// Default component with props
const component = cloneElement(children, { ...props });

const options = compPreview.options.reduce(
(res, cur) => {
if (cur.type === "checkbox") {
res.checkbox.push(cur);
}

if (cur.type === "select") {
res.select.push(cur);
}

return res;
},
{ checkbox: [], select: [] },
);

return (
<>
<div className={`PreviewBlock ${theme}`}>
<div className="PreviewBlock__selects">
<div className="PreviewBlock__selects__content">
{Select ? (
<Select
id="theme"
fieldSize="sm"
onChange={(e) => {
setTheme(
e.target.value === "light"
? "sds-theme-light"
: "sds-theme-dark",
);
}}
>
<option value="light">Light</option>
<option value="dark">Dark</option>
</Select>
) : null}

{options.select.map((o) => renderSelect(o))}
</div>
</div>

<div className="PreviewBlock__component">{component}</div>

<div className="PreviewBlock__checkboxes">
<div className="PreviewBlock__checkboxes__content">
{options.checkbox.map((o) => renderCheckbox(o))}
</div>
</div>
</div>

<PlaygroundEditor>{reactElementToJSXString(component)}</PlaygroundEditor>
</>
);
};
Loading
Loading