Skip to content

Commit

Permalink
feat: add <interactive-example> custom element (#12413)
Browse files Browse the repository at this point in the history
also extracts common interactive example and playground functionality into various web component

https://mozilla-hub.atlassian.net/browse/MP-1806
https://mozilla-hub.atlassian.net/browse/MP-1740
  • Loading branch information
LeoMcA authored Jan 22, 2025
1 parent 19a8f76 commit c02c081
Show file tree
Hide file tree
Showing 27 changed files with 1,156 additions and 381 deletions.
5 changes: 2 additions & 3 deletions client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@
"node"
],
"moduleNameMapper": {
"^.+\\.module\\.(css|sass|scss)$": "identity-obj-proxy"
"\\.s?css(\\?css)?$": "<rootDir>/config/jest/cssTransform.js"
},
"modulePaths": [],
"resetMocks": true,
Expand All @@ -78,8 +78,7 @@
"^(?!.*\\.(js|jsx|mjs|cjs|ts|tsx|css|json)$)": "<rootDir>/config/jest/fileTransform.js"
},
"transformIgnorePatterns": [
"/node_modules/(?!@mozilla/glean/.*)/",
"^.+\\.module\\.(css|sass|scss)$"
"/node_modules/(?!@mozilla/glean/.*)/"
],
"watchPlugins": [
"jest-watch-typeahead/filename",
Expand Down
10 changes: 6 additions & 4 deletions client/src/document/index.scss
Original file line number Diff line number Diff line change
Expand Up @@ -574,6 +574,10 @@ math[display="block"] {
margin-bottom: 2rem;
position: relative;

[class*="interactive-example"] {
display: none;
}

.example-header {
align-items: baseline;
background-color: var(--background-secondary);
Expand Down Expand Up @@ -695,8 +699,7 @@ math[display="block"] {
margin: 0;
text-transform: capitalize;

&:hover,
&:focus {
&:hover {
opacity: 0.6;
}

Expand All @@ -716,8 +719,7 @@ math[display="block"] {
padding: 1px;
text-transform: capitalize;

&:hover,
&:focus {
&:hover {
opacity: 0.6;
}
}
Expand Down
5 changes: 5 additions & 0 deletions client/src/document/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ import "./index.scss";
// code could come with its own styling rather than it having to be part of the
// main bundle all the time.
import "./interactive-examples.scss";
import "../lit/interactive-example.global.scss";
import { DocumentSurvey } from "../ui/molecules/document-survey";
import { useIncrementFrequentlyViewed } from "../plus/collections/frequently-viewed";
import { useInteractiveExamplesActionHandler as useInteractiveExamplesTelemetry } from "../telemetry/interactive-examples";
Expand Down Expand Up @@ -61,6 +62,10 @@ export class HTTPError extends Error {
}

export function Document(props /* TODO: define a TS interface for this */) {
React.useEffect(() => {
import("../lit/interactive-example.js");
}, []);

const gleanClick = useGleanClick();
const isServer = useIsServer();

Expand Down
11 changes: 11 additions & 0 deletions client/src/lit/glean-mixin.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
/**
* @template {new (...args: any[]) => {}} TBase
* @param {TBase} Base
*/
export const GleanMixin = (Base) =>
class extends Base {
/** @param {string} detail */
_gleanClick(detail) {
window.dispatchEvent(new CustomEvent("glean-click", { detail }));
}
};
12 changes: 12 additions & 0 deletions client/src/lit/globals.d.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,26 @@
import { MDNImageHistory, TeamMember } from "./about";
import { InteractiveExample } from "./interactive-example";
import { ContributorList } from "./community/contributor-list";
import { ScrimInline } from "./curriculum/scrim-inline";
import { PlayConsole } from "./play/console";
import { PlayController } from "./play/controller";
import { PlayEditor } from "./play/editor";
import { PlayRunner } from "./play/runner";

declare global {
interface HTMLElementTagNameMap {
"mdn-image-history": MDNImageHistory;
"team-member": TeamMember;
"interactive-example": InteractiveExample;
"contributor-list": ContributorList;
"scrim-inline": ScrimInline;
"play-console": PlayConsole;
"play-controller": PlayController;
"play-editor": PlayEditor;
"play-runner": PlayRunner;
}

interface WindowEventMap {
"glean-click": CustomEvent<string>;
}
}
13 changes: 13 additions & 0 deletions client/src/lit/interactive-example.global.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
interactive-example {
display: block;
height: 513px;
margin: 1rem 0;

&[height="shorter"] {
height: 433px;
}

&[height="taller"] {
height: 725px;
}
}
104 changes: 104 additions & 0 deletions client/src/lit/interactive-example.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import { html, LitElement } from "lit";
import { ref, createRef } from "lit/directives/ref.js";
import "./play/editor.js";
import "./play/controller.js";
import "./play/console.js";
import "./play/runner.js";
import { GleanMixin } from "./glean-mixin.js";

import styles from "./interactive-example.scss?css" with { type: "css" };

/**
* @import { Ref } from 'lit/directives/ref.js';
* @import { PlayController } from "./play/controller.js";
*/

export class InteractiveExample extends GleanMixin(LitElement) {
static styles = styles;

/** @type {Ref<PlayController>} */
_controller = createRef();

_run() {
this._controller.value?.run();
}

_reset() {
this._controller.value?.reset();
}

_initialCode() {
const examples = this.closest("section")?.querySelectorAll(
".code-example pre[class*=interactive-example]"
);
return Array.from(examples || []).reduce((acc, pre) => {
const language = pre.classList[1];
return language && pre.textContent
? {
...acc,
[language]: acc[language]
? `${acc[language]}\n${pre.textContent}`
: pre.textContent,
}
: acc;
}, /** @type {Object<string, string>} */ ({}));
}

/** @param {Event} ev */
_telemetryHandler(ev) {
let action = ev.type;
if (
ev.type === "click" &&
ev.target instanceof HTMLElement &&
ev.target.id
) {
action = `click@${ev.target.id}`;
}
this._gleanClick(`interactive-examples-lit: ${action}`);
}

connectedCallback() {
super.connectedCallback();
this._telemetryHandler = this._telemetryHandler.bind(this);
this.renderRoot.addEventListener("focus", this._telemetryHandler);
this.renderRoot.addEventListener("copy", this._telemetryHandler);
this.renderRoot.addEventListener("cut", this._telemetryHandler);
this.renderRoot.addEventListener("paste", this._telemetryHandler);
this.renderRoot.addEventListener("click", this._telemetryHandler);
}

render() {
return html`
<play-controller ${ref(this._controller)}>
<div class="template-javascript">
<h4>JavaScript Demo:</h4>
<play-editor id="editor" language="javascript"></play-editor>
<div class="buttons">
<button id="execute" @click=${this._run}>Run</button>
<button id="reset" @click=${this._reset}>Reset</button>
</div>
<play-console id="console"></play-console>
<play-runner></play-runner>
</div>
</play-controller>
`;
}

firstUpdated() {
const code = this._initialCode();
if (this._controller.value) {
this._controller.value.code = code;
}
}

disconnectedCallback() {
super.disconnectedCallback();
this.renderRoot.removeEventListener("focus", this._telemetryHandler);
this.renderRoot.removeEventListener("copy", this._telemetryHandler);
this.renderRoot.removeEventListener("cut", this._telemetryHandler);
this.renderRoot.removeEventListener("paste", this._telemetryHandler);
this.renderRoot.removeEventListener("click", this._telemetryHandler);
}
}

customElements.define("interactive-example", InteractiveExample);
72 changes: 72 additions & 0 deletions client/src/lit/interactive-example.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
@use "../ui/vars" as *;
@use "../ui/atoms/button/mixins" as button;

h4 {
border: 1px solid var(--border-secondary);
border-top-left-radius: var(--elem-radius);
border-top-right-radius: var(--elem-radius);
font-size: 1rem;
font-weight: normal;
grid-area: header;
line-height: 1.1876;
margin: 0;
padding: 0.5rem 1rem;
}

play-editor {
border: 1px solid var(--border-secondary);
border-bottom-left-radius: var(--elem-radius);
border-bottom-right-radius: var(--elem-radius);
border-top: none;
grid-area: editor;
margin-top: -0.5rem;
overflow: auto;
}

.buttons {
display: flex;
flex-direction: column;
gap: 0.5rem;
grid-area: buttons;

button {
@include button.secondary;
}
}

play-console {
border: 1px solid var(--border-secondary);
border-radius: var(--elem-radius);
grid-area: console;
}

.template-javascript {
align-content: start;
display: grid;
gap: 0.5rem;
grid-template-areas:
"header header"
"editor editor"
"buttons console";
grid-template-columns: max-content 1fr;
grid-template-rows: max-content 1fr;
height: 100%;

play-runner {
display: none;
}

@media (max-width: $screen-sm) {
grid-template-areas:
"header"
"editor"
"buttons"
"console";
grid-template-columns: 1fr;

.buttons {
flex-direction: row;
justify-content: space-between;
}
}
}
Loading

0 comments on commit c02c081

Please sign in to comment.