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

feat(playground): add prop to display console messages from demo #3060

Merged
merged 15 commits into from
Aug 10, 2023
92 changes: 89 additions & 3 deletions src/components/global/Playground/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import React, { useEffect, useMemo, useRef, useState } from 'react';
import useBaseUrl from '@docusaurus/useBaseUrl';
import './playground.css';
import { EditorOptions, openAngularEditor, openHtmlEditor, openReactEditor, openVueEditor } from './stackblitz.utils';
import { Mode, UsageTarget } from './playground.types';
import { ConsoleItem, Mode, UsageTarget } from './playground.types';
import useThemeContext from '@theme/hooks/useThemeContext';

import Tippy from '@tippyjs/react';
Expand Down Expand Up @@ -109,6 +109,7 @@ interface UsageTargetOptions {
* @param src The absolute path to the playground demo. For example: `/usage/button/basic/demo.html`
* @param size The height of the playground. Supports `xsmall`, `small`, `medium`, `large`, 'xlarge' or any string value.
* @param devicePreview `true` if the playground example should render in a device frame (iOS/MD).
* @param showConsole `true` if the playground should render a console UI that reflects console logs, warnings, and errors.
*/
export default function Playground({
code,
Expand All @@ -118,6 +119,7 @@ export default function Playground({
size = 'small',
mode,
devicePreview,
showConsole,
includeIonContent = true,
version,
}: {
Expand All @@ -133,6 +135,7 @@ export default function Playground({
mode?: 'ios' | 'md';
description?: string;
devicePreview?: boolean;
showConsole?: boolean;
includeIonContent: boolean;
/**
* The major version of Ionic to use in the generated Stackblitz examples.
Expand All @@ -159,6 +162,7 @@ export default function Playground({
const codeRef = useRef(null);
const frameiOS = useRef<HTMLIFrameElement | null>(null);
const frameMD = useRef<HTMLIFrameElement | null>(null);
const consoleBodyRef = useRef<HTMLDivElement | null>(null);

const defaultMode = typeof mode !== 'undefined' ? mode : Mode.iOS;

Expand All @@ -182,6 +186,15 @@ export default function Playground({
const [codeSnippets, setCodeSnippets] = useState({});
const [renderIframes, setRenderIframes] = useState(false);
const [iframesLoaded, setIframesLoaded] = useState(false);
const [mdConsoleItems, setMDConsoleItems] = useState<ConsoleItem[]>([]);
const [iosConsoleItems, setiOSConsoleItems] = useState<ConsoleItem[]>([]);

/**
* We don't actually care about the count, but this lets us
* re-trigger useEffect hooks when the demo is reset and the
* iframes are refreshed.
*/
const [resetCount, setResetCount] = useState(0);

/**
* Rather than encode isDarkTheme into the frame source
Expand Down Expand Up @@ -258,6 +271,24 @@ export default function Playground({
setFramesLoaded();
}, [renderIframes]);

useEffect(() => {
if (showConsole) {
if (frameiOS.current) {
frameiOS.current.contentWindow.addEventListener('console', (ev: CustomEvent) => {
setiOSConsoleItems((oldConsoleItems) => [...oldConsoleItems, ev.detail]);
consoleBodyRef.current.scrollTo(0, consoleBodyRef.current.scrollHeight);
});
}

if (frameMD.current) {
frameMD.current.contentWindow.addEventListener('console', (ev: CustomEvent) => {
setMDConsoleItems((oldConsoleItems) => [...oldConsoleItems, ev.detail]);
consoleBodyRef.current.scrollTo(0, consoleBodyRef.current.scrollHeight);
});
}
}
}, [iframesLoaded, resetCount]); // including resetCount re-runs this when iframes are reloaded

useEffect(() => {
/**
* Using a dynamic import here to avoid SSR errors when trying to extend `HTMLElement`
Expand Down Expand Up @@ -311,13 +342,19 @@ export default function Playground({
/**
* Reloads the iOS and MD iframe sources back to their original state.
*/
function resetDemo() {
async function resetDemo() {
if (frameiOS.current) {
frameiOS.current.contentWindow.location.reload();
}
if (frameMD.current) {
frameMD.current.contentWindow.location.reload();
}

setiOSConsoleItems([]);
setMDConsoleItems([]);

await Promise.all([waitForNextFrameLoadEvent(frameiOS.current), waitForNextFrameLoadEvent(frameMD.current)]);
setResetCount((oldCount) => oldCount + 1);
}

function openEditor(event) {
Expand Down Expand Up @@ -444,11 +481,39 @@ export default function Playground({
);
}

function renderConsole() {
const consoleItems = ionicMode === Mode.iOS ? iosConsoleItems : mdConsoleItems;

return (
<div className="playground__console">
<div className="playground__console-header">
<code>Console</code>
</div>
<div className="playground__console-body" ref={consoleBodyRef}>
{consoleItems.length === 0 ? (
<div className="playground__console-item playground__console-item--placeholder">
<code>Console messages will appear here when logged from the example above.</code>
</div>
) : (
consoleItems.map((consoleItem, i) => (
<div key={i} className={`playground__console-item playground__console-item--${consoleItem.type}`}>
{consoleItem.type !== 'log' && (
<div className="playground__console-icon">{consoleItem.type === 'warning' ? '⚠' : '❌'}</div>
)}
<code>{consoleItem.message}</code>
</div>
))
)}
</div>
</div>
);
}

const sortedUsageTargets = useMemo(() => Object.keys(UsageTarget).sort(), []);

return (
<div className="playground" ref={hostRef}>
<div className="playground__container">
<div className={`playground__container ${showConsole ? 'playground__container--has-console' : ''}`}>
<div className="playground__control-toolbar">
<div className="playground__control-group">
{sortedUsageTargets.map((lang) => {
Expand Down Expand Up @@ -633,6 +698,7 @@ export default function Playground({
]
: []}
</div>
{showConsole && renderConsole()}
<div ref={codeRef} className="playground__code-block">
{renderCodeSnippets()}
</div>
Expand Down Expand Up @@ -660,6 +726,26 @@ const waitForFrame = (frame: HTMLIFrameElement) => {
});
};

/**
* Returns a promise that resolves on the *next* load event of the
* given iframe. We intentionally don't check if it's already loaded
* because this is used when the demo is reset and the iframe is
* refreshed, so we don't want to return too early and catch the
* pre-reset version of the window.
*/
const waitForNextFrameLoadEvent = (frame: HTMLIFrameElement) => {
return new Promise<void>((resolve) => {
const handleLoad = () => {
frame.removeEventListener('load', handleLoad);
resolve();
};

if (frame) {
frame.addEventListener('load', handleLoad);
}
});
};

const isFrameReady = (frame: HTMLIFrameElement) => {
if (!frame) {
return false;
Expand Down
108 changes: 108 additions & 0 deletions src/components/global/Playground/playground.css
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,14 @@
--playground-tabs-background: var(--c-carbon-90);
--playground-tab-btn-color: var(--c-carbon-20);
--playground-tab-btn-border-color: transparent;

--playground-console-item-separator-color: var(--c-carbon-80);
--playground-console-warning-background: #332B00;
--playground-console-warning-color: var(--c-yellow-80);
--playground-console-warning-separator-color: #665500;
--playground-console-error-background: #290000;
--playground-console-error-color: var(--c-red-40);
--playground-console-error-separator-color: #5C0000;
}

.playground {
Expand All @@ -28,6 +36,13 @@
* @prop --playground-tabs-background: The background color of the tabs bar not including the active tab button.
* @prop --playground-tab-btn-color: The text color of the tab buttons.
* @prop --playground-tab-btn-border-color: The border color of the tab buttons.
* @prop --playground-console-item-separator-color The color of the separator/border between console UI items.
* @prop --playground-console-warning-background The background color of warning items in the console UI.
* @prop --playground-console-warning-color The text color of warning items in the console UI.
* @prop --playground-console-warning-separator-color The color of the top and bottom separator/border for warning items in the console UI.
* @prop --playground-console-error-background The background color of error items in the console UI.
* @prop --playground-console-error-color The text color of error items in the console UI.
* @prop --playground-console-error-separator-color The color of the top and bottom separator/border for error items in the console UI.
*/
--playground-btn-color: var(--c-indigo-90);
--playground-btn-selected-color: var(--c-blue-90);
Expand All @@ -41,6 +56,14 @@
--playground-tab-btn-color: var(--c-carbon-100);
--playground-tab-btn-border-color: var(--c-indigo-30);

--playground-console-item-separator-color: var(--c-indigo-20);
--playground-console-warning-background: var(--c-yellow-10);
--playground-console-warning-color: #5C3C00;
--playground-console-warning-separator-color: var(--c-yellow-30);
--playground-console-error-background: var(--c-red-10);
--playground-console-error-color: var(--c-red-90);
--playground-console-error-separator-color: var(--c-red-30);

overflow: hidden;

margin-bottom: var(--ifm-leading);
Expand All @@ -52,6 +75,11 @@
border-radius: var(--ifm-code-border-radius);
}

.playground__container--has-console {
border-bottom-left-radius: 0;
border-bottom-right-radius: 0;
}

/* Playground preview contains the demo example*/
.playground__preview {
display: flex;
Expand Down Expand Up @@ -213,6 +241,86 @@
}
}

.playground__console {
averyjohnston marked this conversation as resolved.
Show resolved Hide resolved
background-color: var(--code-block-bg-c);
border: 1px solid var(--playground-separator-color);
border-top: 0;
border-bottom-left-radius: var(--ifm-code-border-radius);
border-bottom-right-radius: var(--ifm-code-border-radius);
}

.playground__console-header {
background-color: var(--playground-separator-color);
font-weight: bold;
text-transform: uppercase;
}

.playground__console-body {
overflow-y: auto;

height: 120px;
}

.playground__console-item {
border-top: 1px solid var(--separator-color);

position: relative;
}

.playground__console-header, .playground__console-item {
padding: 3px 3px 3px 28px;
}

.playground__console-item:first-child {
border-top: none;
}

.playground__console-item:last-child {
border-bottom: 1px solid var(--separator-color);
}

.playground__console-item--placeholder {
font-style: italic;
}

.playground__console-item--log {
--separator-color: var(--playground-console-item-separator-color);
}

.playground__console-item--warning {
--separator-color: var(--playground-console-warning-separator-color);
background-color: var(--playground-console-warning-background);
border-bottom: 1px solid var(--separator-color);
color: var(--playground-console-warning-color);
}

.playground__console-item--error {
--separator-color: var(--playground-console-error-separator-color);
background-color: var(--playground-console-error-background);
border-bottom: 1px solid var(--separator-color);
color: var(--playground-console-error-color);
}

/* warnings and errors have both borders colored, so hide the extra from the neighboring item */
.playground__console-item--warning + .playground__console-item,
.playground__console-item--error + .playground__console-item {
border-top: none;
}

.playground__console-icon {
position: absolute;
top: 3px;
left: 3px;
}

.playground__console code {
background-color: transparent;
font-size: 0.813rem;
padding: 0;
padding-block-start: 0; /* prevents text getting cut off vertically */
padding-block-end: 0; /* prevents border from item below getting covered up */
}

/** Tabs **/
.playground .tabs-container {
background: var(--playground-code-background);
Expand Down
5 changes: 5 additions & 0 deletions src/components/global/Playground/playground.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,8 @@ export enum Mode {
iOS = 'ios',
MD = 'md',
}

export interface ConsoleItem {
type: 'log' | 'warning' | 'error';
message: string;
}
35 changes: 35 additions & 0 deletions static/usage/common.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,41 @@ window.addEventListener('DOMContentLoaded', () => {
}
});

/**
* Monkey-patch the console methods so we can dispatch
* events when they're called, allowing the data to be
* captured by the playground's console UI.
*/
const _log = console.log,
_warn = console.warn,
_error = console.error;

const dispatchConsoleEvent = (type, arguments) => {
window.dispatchEvent(
new CustomEvent('console', {
detail: {
type,
message: Object.values(arguments).join(' '),
},
})
);
};

console.log = function () {
dispatchConsoleEvent('log', arguments);
return _log.apply(console, arguments);
};

console.warn = function () {
dispatchConsoleEvent('warning', arguments);
return _warn.apply(console, arguments);
};

console.error = function () {
dispatchConsoleEvent('error', arguments);
return _error.apply(console, arguments);
};

/**
* The Playground needs to wait for the message listener
* to be created before sending any messages, otherwise
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
```html
<ion-range aria-label="Range with ionChange" (ionChange)="onIonChange($event)"></ion-range>
<ion-label>ionChange emitted value: {{ lastEmittedValue }}</ion-label>
```
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,14 @@
import { Component } from '@angular/core';

import { RangeCustomEvent } from '@ionic/angular';
import { RangeValue } from '@ionic/core';

@Component({
selector: 'app-example',
templateUrl: 'example.component.html',
})
export class ExampleComponent {
lastEmittedValue: RangeValue;

onIonChange(ev: Event) {
this.lastEmittedValue = (ev as RangeCustomEvent).detail.value;
console.log('ionChange emitted value:', (ev as RangeCustomEvent).detail.value);
}
}
```
Loading
Loading