diff --git a/src/components/global/Playground/index.tsx b/src/components/global/Playground/index.tsx index 0681230b851..c7ab14d5a35 100644 --- a/src/components/global/Playground/index.tsx +++ b/src/components/global/Playground/index.tsx @@ -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'; @@ -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, @@ -118,6 +119,7 @@ export default function Playground({ size = 'small', mode, devicePreview, + showConsole, includeIonContent = true, version, }: { @@ -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. @@ -159,6 +162,7 @@ export default function Playground({ const codeRef = useRef(null); const frameiOS = useRef(null); const frameMD = useRef(null); + const consoleBodyRef = useRef(null); const defaultMode = typeof mode !== 'undefined' ? mode : Mode.iOS; @@ -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([]); + const [iosConsoleItems, setiOSConsoleItems] = useState([]); + + /** + * 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 @@ -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` @@ -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) { @@ -444,11 +481,39 @@ export default function Playground({ ); } + function renderConsole() { + const consoleItems = ionicMode === Mode.iOS ? iosConsoleItems : mdConsoleItems; + + return ( +
+
+ Console +
+
+ {consoleItems.length === 0 ? ( +
+ Console messages will appear here when logged from the example above. +
+ ) : ( + consoleItems.map((consoleItem, i) => ( +
+ {consoleItem.type !== 'log' && ( +
{consoleItem.type === 'warning' ? '⚠' : '❌'}
+ )} + {consoleItem.message} +
+ )) + )} +
+
+ ); + } + const sortedUsageTargets = useMemo(() => Object.keys(UsageTarget).sort(), []); return (
-
+
{sortedUsageTargets.map((lang) => { @@ -633,6 +698,7 @@ export default function Playground({ ] : []}
+ {showConsole && renderConsole()}
{renderCodeSnippets()}
@@ -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((resolve) => { + const handleLoad = () => { + frame.removeEventListener('load', handleLoad); + resolve(); + }; + + if (frame) { + frame.addEventListener('load', handleLoad); + } + }); +}; + const isFrameReady = (frame: HTMLIFrameElement) => { if (!frame) { return false; diff --git a/src/components/global/Playground/playground.css b/src/components/global/Playground/playground.css index 46e77dc40c0..79e33680792 100644 --- a/src/components/global/Playground/playground.css +++ b/src/components/global/Playground/playground.css @@ -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 { @@ -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); @@ -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); @@ -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; @@ -213,6 +241,86 @@ } } +.playground__console { + 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); diff --git a/src/components/global/Playground/playground.types.ts b/src/components/global/Playground/playground.types.ts index 20d98474eec..c6a14b8e585 100644 --- a/src/components/global/Playground/playground.types.ts +++ b/src/components/global/Playground/playground.types.ts @@ -9,3 +9,8 @@ export enum Mode { iOS = 'ios', MD = 'md', } + +export interface ConsoleItem { + type: 'log' | 'warning' | 'error'; + message: string; +} diff --git a/static/usage/common.js b/static/usage/common.js index 6bcd8833a30..52694d7e7b2 100644 --- a/static/usage/common.js +++ b/static/usage/common.js @@ -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 diff --git a/static/usage/v7/range/ion-change-event/angular/example_component_html.md b/static/usage/v7/range/ion-change-event/angular/example_component_html.md index 12a736de8dd..bbcbd120285 100644 --- a/static/usage/v7/range/ion-change-event/angular/example_component_html.md +++ b/static/usage/v7/range/ion-change-event/angular/example_component_html.md @@ -1,4 +1,3 @@ ```html -ionChange emitted value: {{ lastEmittedValue }} ``` diff --git a/static/usage/v7/range/ion-change-event/angular/example_component_ts.md b/static/usage/v7/range/ion-change-event/angular/example_component_ts.md index b90eff62d29..377ccd2dc28 100644 --- a/static/usage/v7/range/ion-change-event/angular/example_component_ts.md +++ b/static/usage/v7/range/ion-change-event/angular/example_component_ts.md @@ -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); } } ``` diff --git a/static/usage/v7/range/ion-change-event/demo.html b/static/usage/v7/range/ion-change-event/demo.html index 0c5f17d2875..42fa2654800 100644 --- a/static/usage/v7/range/ion-change-event/demo.html +++ b/static/usage/v7/range/ion-change-event/demo.html @@ -22,17 +22,15 @@
- ionChange emitted value:
diff --git a/static/usage/v7/range/ion-change-event/index.md b/static/usage/v7/range/ion-change-event/index.md index c35324b9448..b61d73279f3 100644 --- a/static/usage/v7/range/ion-change-event/index.md +++ b/static/usage/v7/range/ion-change-event/index.md @@ -9,6 +9,7 @@ import angular_example_component_ts from './angular/example_component_ts.md'; -ionChange emitted value: ``` diff --git a/static/usage/v7/range/ion-change-event/react.md b/static/usage/v7/range/ion-change-event/react.md index d6c43bc7696..d49b2adb596 100644 --- a/static/usage/v7/range/ion-change-event/react.md +++ b/static/usage/v7/range/ion-change-event/react.md @@ -1,17 +1,13 @@ ```tsx -import React, { useState } from 'react'; -import { IonLabel, IonRange } from '@ionic/react'; -import { RangeValue } from '@ionic/core'; +import React from 'react'; +import { IonRange } from '@ionic/react'; + function Example() { - const [lastEmittedValue, setLastEmittedValue] = useState(); return ( - <> - setLastEmittedValue(detail.value)} - > - ionChange emitted value: {lastEmittedValue as number} - + console.log('ionChange emitted value: ' + detail.value)} + > ); } export default Example; diff --git a/static/usage/v7/range/ion-change-event/vue.md b/static/usage/v7/range/ion-change-event/vue.md index 306682ab7db..44a3fdcfa1f 100644 --- a/static/usage/v7/range/ion-change-event/vue.md +++ b/static/usage/v7/range/ion-change-event/vue.md @@ -1,23 +1,17 @@ ```html