diff --git a/packages/wordpress-playground-block/README.md b/packages/wordpress-playground-block/README.md index 665917cc..250e06a8 100644 --- a/packages/wordpress-playground-block/README.md +++ b/packages/wordpress-playground-block/README.md @@ -64,6 +64,7 @@ In order to contribute to `wordpress-playground-block`, you'll need to first ins - Make sure you have `nvm` installed. If you need to install it first, [follow these installation instructions](https://github.com/nvm-sh/nvm#installation). +- Install wp-now globally to power the local development environment by running `npm install -g @wp-now/wp-now` - Install `nx` by running `npm install -g nx`. Once the global dependencies are installed, you can start using the repo: diff --git a/packages/wordpress-playground-block/README.plugindirectory.txt b/packages/wordpress-playground-block/README.plugindirectory.txt index fbe646a9..92555f12 100644 --- a/packages/wordpress-playground-block/README.plugindirectory.txt +++ b/packages/wordpress-playground-block/README.plugindirectory.txt @@ -3,7 +3,7 @@ Contributors: wordpressdotorg, dawidurbanski, zieladam Tags: code, interactive, playground, block Requires at least: 6.0 Tested up to: 6.6 -Stable tag: 0.2.13 +Stable tag: 0.2.17 Requires PHP: 7.0 License: GPLv2 License URI: https://www.gnu.org/licenses/gpl-2.0.html diff --git a/packages/wordpress-playground-block/src/components/playground-preview/download-zipped-plugin.ts b/packages/wordpress-playground-block/src/components/playground-preview/download-zipped-package.ts similarity index 80% rename from packages/wordpress-playground-block/src/components/playground-preview/download-zipped-plugin.ts rename to packages/wordpress-playground-block/src/components/playground-preview/download-zipped-package.ts index 07b24f32..e77ae3db 100644 --- a/packages/wordpress-playground-block/src/components/playground-preview/download-zipped-plugin.ts +++ b/packages/wordpress-playground-block/src/components/playground-preview/download-zipped-package.ts @@ -4,12 +4,23 @@ import { // @ts-ignore } from 'https://playground.wordpress.net/client/index.js'; -export default async function downloadZippedPlugin(client: PlaygroundClient) { +export default async function downloadZippedPackage( + client: PlaygroundClient, + codeEditorMode +) { const docroot = await client.documentRoot; - const pluginPath = docroot + '/wp-content/plugins/demo-plugin'; + + let pluginPath = docroot + '/wp-content/plugins/demo-plugin'; + let fileName = 'wordpress-playground-plugin.zip'; + + if (codeEditorMode == 'theme') { + pluginPath = docroot + '/wp-content/themes/demo-theme'; + fileName = 'wordpress-playground-theme.zip'; + } + const zipFile = new File( [await zipPlaygroundFiles(client, pluginPath)], - 'wordpress-playground-plugin.zip', + fileName, { type: 'application/zip', } diff --git a/packages/wordpress-playground-block/src/components/playground-preview/index.tsx b/packages/wordpress-playground-block/src/components/playground-preview/index.tsx index 7dd7629c..105d8419 100644 --- a/packages/wordpress-playground-block/src/components/playground-preview/index.tsx +++ b/packages/wordpress-playground-block/src/components/playground-preview/index.tsx @@ -19,7 +19,7 @@ import { useState, createInterpolateElement, } from '@wordpress/element'; -import { Button, Spinner } from '@wordpress/components'; +import { Button, Spinner, withSpokenMessages } from '@wordpress/components'; import { Icon, plus, @@ -32,7 +32,8 @@ import { import useEditorFiles, { isErrorLogFile } from './use-editor-files'; import { LanguageSupport } from '@codemirror/language'; import { writePluginFiles } from './write-plugin-files'; -import downloadZippedPlugin from './download-zipped-plugin'; +import { writeThemeFiles } from './write-theme-files'; +import downloadZippedPackage from './download-zipped-package'; import classnames from 'classnames'; import FileManagementModals, { FileManagerRef } from './file-management-modals'; import { @@ -49,6 +50,7 @@ export type PlaygroundDemoProps = Attributes & { inFullPageView?: boolean; baseAttributesForFullPageView?: object; onStateChange?: (state: any) => void; + speak: (message: string, ariaLive?: string) => void; }; const languages: Record = { @@ -83,12 +85,13 @@ function getRefreshPath(lastPath: string) { return url.pathname + url.search; } -export default function PlaygroundPreview({ +function PlaygroundPreview({ inBlockEditor, blueprint, blueprintUrl, configurationSource, codeEditor, + codeEditorMode, codeEditorSideBySide, codeEditorReadOnly, codeEditorTranspileJsx, @@ -112,6 +115,7 @@ export default function PlaygroundPreview({ inFullPageView = false, baseAttributesForFullPageView = {}, onStateChange, + speak, }: PlaygroundDemoProps) { const { files, @@ -135,6 +139,7 @@ export default function PlaygroundPreview({ const afterPreviewRef = useRef(null); const playgroundClientRef = useRef(null); const fileMgrRef = useRef(null); + const downloadButtonRef = useRef(null); const codeMirrorRef = useRef(null); /** @@ -169,19 +174,19 @@ export default function PlaygroundPreview({ const [dismissedExitWithKeyboardTip, setDismissedExitWithKeyboardTip] = useState(localStorage[dismissedExitWithKeyboardTipKey] === 'true'); function dismissExitWithKeyboardTip() { + // Shift focus to previous focusable control + // so focus is not lost as the tip disappears + if (downloadButtonRef?.current) { + downloadButtonRef.current.focus(); + } + localStorage[dismissedExitWithKeyboardTipKey] = 'true'; setDismissedExitWithKeyboardTip(true); - - // Shift focus to editor so focus is not lost as the tip disappears - if (codeMirrorRef?.current?.view?.dom) { - const contentEditableElement: HTMLElement = - codeMirrorRef.current.view.dom.querySelector( - '[contenteditable=true]' - ); - if (contentEditableElement) { - contentEditableElement.focus(); - } - } + speak( + // translators: This describes a UI notice that has been dismissed by the user. + __('Notice dismissed.'), + 'polite' + ); } /** @@ -259,7 +264,22 @@ export default function PlaygroundPreview({ await client.isReady(); playgroundClientRef.current = client; - await reinstallEditedPlugin(); + // Hack: Delay the announcement to give iframe loading percentage + // announcements for the iframe a chance to be queued before this + // "loading complete" announcement. Without this, macOS VoiceOver + // often speaks "WordPress Playground loaded. 10% loaded" which + // is a miscommunication because Playground has already loaded. + setTimeout( + () => + speak( + // translators: This says that the Playground preview has loaded. + __('WordPress Playground loaded.'), + 'polite' + ), + 500 + ); + + await reinstallEditedCode(); if (configurationSource === 'block-attributes') { let postId = 0; @@ -345,7 +365,7 @@ export default function PlaygroundPreview({ const [transpilationFailures, setTranspilationFailures] = useState< TranspilationFailure[] >([]); - async function reinstallEditedPlugin() { + async function reinstallEditedCode() { if (!playgroundClientRef.current || !codeEditor) { return; } @@ -354,6 +374,7 @@ export default function PlaygroundPreview({ const client = playgroundClientRef.current; let finalFiles = files; + if (codeEditorTranspileJsx) { const { failures, transpiledFiles } = await transpilePluginFiles( finalFiles @@ -370,12 +391,17 @@ export default function PlaygroundPreview({ } finalFiles = transpiledFiles; } - await writePluginFiles(client, finalFiles); + + if (codeEditorMode === 'theme') { + await writeThemeFiles(client, finalFiles); + } else { + await writePluginFiles(client, finalFiles); + } } const handleReRunCode = useCallback(() => { async function doHandleRun() { - await reinstallEditedPlugin(); + await reinstallEditedCode(); // Refresh Playground iframe const lastPath = await playgroundClientRef.current!.getCurrentURL(); @@ -388,7 +414,7 @@ export default function PlaygroundPreview({ } else { doHandleRun(); } - }, [reinstallEditedPlugin]); + }, [reinstallEditedCode]); const keymapExtension = useMemo( () => @@ -428,31 +454,24 @@ export default function PlaygroundPreview({ 'WordPress website which may be a challenge for screen readers.' ); + const activeStatusLabel = playgroundClientRef.current + ? // translators: State of the playground iframe after it has loaded. + __('Loaded') + : // translators: State of the playground iframe while it is loading. + __('Loading'); + // translators: State of the playground iframe before the user activates it. + const inactivateStatusLabel = __('Not Activated'); + const beforePlaygroundPreviewLabel = sprintf( + // translators: %s: status of the Playground preview + __('Beginning of Playground Preview - %s'), + isLivePreviewActivated ? activeStatusLabel : inactivateStatusLabel + ); + return (
-
- {!inBlockEditor && !inFullPageView && ( - - )} -
{codeEditor && (
@@ -535,13 +554,15 @@ export default function PlaygroundPreview({ )} + )}
); } + +export default withSpokenMessages(PlaygroundPreview); diff --git a/packages/wordpress-playground-block/src/components/playground-preview/write-theme-files.ts b/packages/wordpress-playground-block/src/components/playground-preview/write-theme-files.ts new file mode 100644 index 00000000..5d331f00 --- /dev/null +++ b/packages/wordpress-playground-block/src/components/playground-preview/write-theme-files.ts @@ -0,0 +1,36 @@ +import type { EditorFile } from '../../index'; +import { + PlaygroundClient, + activateTheme, + // @ts-ignore +} from 'https://playground.wordpress.net/client/index.js'; + +export const writeThemeFiles = async ( + client: PlaygroundClient, + files: EditorFile[] +) => { + const docroot = await client.documentRoot; + const themeFolderName = 'demo-theme'; + const themePath = docroot + '/wp-content/themes/' + themeFolderName; + if (await client.fileExists(themePath)) { + await client.rmdir(themePath, { + recursive: true, + }); + } + await client.mkdir(themePath); + + for (const file of files) { + const filePath = `${themePath}/${file.name}`; + const parentDir = filePath.split('/').slice(0, -1).join('/'); + await client.mkdir(parentDir); + await client.writeFile(filePath, file.contents); + } + + try { + await activateTheme(client, { + themeFolderName, + }); + } catch (e) { + console.error(e); + } +}; diff --git a/packages/wordpress-playground-block/src/edit.tsx b/packages/wordpress-playground-block/src/edit.tsx index 8b5d0ba4..e82d633d 100644 --- a/packages/wordpress-playground-block/src/edit.tsx +++ b/packages/wordpress-playground-block/src/edit.tsx @@ -6,6 +6,7 @@ import { useState, useRef } from '@wordpress/element'; import { ToggleControl, SelectControl, + RadioControl, TextareaControl, Panel, PanelBody, @@ -143,6 +144,8 @@ export default withBase64Attrs(function Edit({ requireLivePreviewActivation, } = attributes; + const showJSXTranspileOption = codeEditorMode === 'plugin'; + return (
- { - setAttributes({ - codeEditorTranspileJsx: - !codeEditorTranspileJsx, - }); + { + if (value === 'theme') { + setAttributes({ + codeEditorMode: value, + codeEditorTranspileJsx: false, + }); + } else { + setAttributes({ + codeEditorMode: value, + }); + } }} /> + {showJSXTranspileOption && ( + { + setAttributes({ + codeEditorTranspileJsx: + !codeEditorTranspileJsx, + }); + }} + /> + )}