Skip to content

Conversation

@DNin01
Copy link
Member

@DNin01 DNin01 commented Sep 14, 2025

Resolves #7230

Changes

  • Adds a new addon which adds a context menu to the green flag. It lets you toggle turbo mode, higher framerate, and mute. Now mobile users can finally use turbo mode without having to enter the editor!
  • Related addons have been updated to utilize their context menu items.
  • Also fixed an issue that prevents the displayNoneWhileDisabled API from working on addons beginning with a number.

It looks like this:
Green flag context menu

*Context menu items (other than toggle turbo mode) only appear if their respective addons are enabled.

Tests

Tested on Edge 140 with a mouse and with DevTools Device Emulation.

To do:

  • Use touchstart instead of contextmenu
  • Localize
  • Dynamic disable, enable, and setting changes work for all updated addons
  • editor-dark-mode compatibility
  • editor-compact compatibility

@DNin01 DNin01 added type: enhancement New feature for the project new addon Related to new addons to this extension. `scope: addons` should still be added. scope: addon Related to one or multiple addons platform: mobile Related to compatibility with touch screen devices labels Sep 14, 2025
@DNin01 DNin01 requested a review from Samq64 September 15, 2025 02:51
Copy link
Member

@mxmou mxmou left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This addon should be enabled by default - otherwise touchscreen users can't use 60fps and mute-project at all without enabling it first.

After the React 18 update, it will be possible to navigate Scratch's context menus using arrow keys. It would be nice if the addon replicated that.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A notice explaining how to access 60 FPS mode on a touch screen would be helpful. Same with mute-project.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That addon already has so much notice text. 🤣

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe add this information to the description instead?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Platform-specific descriptions, which I suggested in #3930 (comment), would also be useful here.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you want to add "touchscreenOnly": true" to notices within this PR it's fine to me.

@DNin01 DNin01 removed the request for review from Samq64 September 15, 2025 16:20
@DNin01
Copy link
Member Author

DNin01 commented Sep 15, 2025

This addon should be enabled by default.

Perhaps enabled by default only for mobile versions of the extension?

That's too much work, though. I'll just make it enabled by default.

After the React 18 update, it will be possible to navigate Scratch's context menus using arrow keys. It would be nice if the addon replicated that.

Ideally, I would make the context menu an instance of a React context menu so that React can take care of this. Not sure how, though. May just have to stick with custom handlers.

We don't use React within our project, do we? (We probably should.)

@DNin01
Copy link
Member Author

DNin01 commented Sep 15, 2025

Okay, I got all those kinks worked out!

@DNin01 DNin01 requested a review from mxmou September 15, 2025 22:03
@mxmou
Copy link
Member

mxmou commented Sep 16, 2025

Ideally, I would make the context menu an instance of a React context menu so that React can take care of this. Not sure how, though. May just have to stick with custom handlers.

Writing the event handlers is probably easier, especially because we currently need to support both versions of React. That's what I did when updating the context menu API. After the React 18 update is out, we can experiment with using Scratch's context menu component.

Speaking of React 18: if you don't mind, I'll push some changes to make the addon's styling work there.

@DNin01
Copy link
Member Author

DNin01 commented Sep 16, 2025

Speaking of React 18: if you don't mind, I'll push some changes to make the addon's styling work there.

Sure, you know more about it than I do.

@DNin01 DNin01 added this to the v1.45.0 milestone Oct 6, 2025
@DNin01 DNin01 added the scope: addon api Related to the addon.* JS APIs or other ways for addons to provide features label Oct 6, 2025
let greenFlag;

const contextMenuClass =
addon.tab.scratchClass("context-menu_context-menu") || addon.tab.scratchClass("context-menu_context-menu-content"); // React 16 || React 18
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Scratch updated React to 18 with the Face Sensing update, so this can be simplified. The style of the context menu was also changed, so some things may need to be tweaked.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I mean, Scratch's new context menu has no transition for fading in/out, it just appears immediately, plus there's arrow-key navigation and the width was reduced. Every other property applies automatically.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Scratch updated React to 18 with the Face Sensing update, so this can be simplified.

Done. I've also removed the opacity transition.

The React 18 update has been released, so we no longer need to support React 16.
@DNin01
Copy link
Member Author

DNin01 commented Oct 8, 2025

After the React 18 update, it will be possible to navigate Scratch's context menus using arrow keys. It would be nice if the addon replicated that.

I don't know if Scratch uses this anymore, but react-contextmenu is in the package.json. If we used that library...

I know how to program arrow-key navigation myself, though.

@DNin01
Copy link
Member Author

DNin01 commented Oct 8, 2025

Still working on an implementation that skips over invisible menu items, but here's what I have now. I think it's alright.
  export default async function ({ addon, console, msg }) {
    let greenFlag;
+   let contextMenuOpen = false;

    const contextMenu = Object.assign(document.createElement("nav"), {
      role: "menu",
      className: addon.tab.scratchClass("context-menu_context-menu-content", { others: "sa-flag-context-menu" }),
    });
+   const setHighlightedItem = (menuitem) => {
+     const currentItem = contextMenu.querySelector("[data-highlighted]");
+     if (currentItem) {
+       currentItem.removeAttribute("data-highlighted");
+       currentItem.tabIndex = -1;
+     }
+     if (menuitem) {
+       menuitem.setAttribute("data-highlighted", "");
+       menuitem.tabIndex = 0;
+       menuitem.focus();
+     }
+   }
    const createItem = (id, textContent = "") => {
      const item = Object.assign(document.createElement("div"), {
        role: "menuitem",
        className: addon.tab.scratchClass("context-menu_menu-item"),
        id,
        textContent,
      });
-     item.addEventListener("mouseenter", () => item.setAttribute("data-highlighted", ""));
-     item.addEventListener("mouseleave", () => item.removeAttribute("data-highlighted"));
+     item.addEventListener("mouseenter", () => setHighlightedItem(item));
+     item.addEventListener("mouseleave", () => setHighlightedItem());
      return item;
    };
    contextMenu.append(
      createItem("sa-flag-menu-turbo", msg("turbo-on")),
      createItem("sa-flag-menu-fps"),
      createItem("sa-flag-menu-mute")
    );

    function closeContextMenu() {
+     contextMenuOpen = false;
+     setHighlightedItem();
      contextMenu.classList.remove("sa-flag-menu-open");
    }
    document.addEventListener("mousedown", (e) => {
      if (!e.target.closest("[role='menu']")) closeContextMenu();
    });
    document.addEventListener("keydown", (e) => {
-     if (e.key === "Escape") closeContextMenu();
+     if (!contextMenuOpen) return;
+     switch (e.key) {
+       case "Escape":
+       case "Tab":
+         closeContextMenu();
+       break;
+       case "Enter":
+       case " ":
+         if (contextMenu.querySelector("[data-highlighted]")) contextMenu.querySelector("[data-highlighted]").click();
+       break;
+       case "ArrowUp":
+         const previousItem = contextMenu.querySelector("[data-highlighted]")?.previousElementSibling ?? contextMenu.lastElementChild;
+         setHighlightedItem(previousItem, true);
+       break;
+       case "ArrowDown":
+         const nextItem = contextMenu.querySelector("[data-highlighted]")?.nextElementSibling ?? contextMenu.firstElementChild;
+         setHighlightedItem(nextItem, true);
+       break;
+       case "Home":
+       case "PageUp":
+         setHighlightedItem(contextMenu.firstElementChild, true);
+       break;
+       case "End":
+       case "PageDown":
+         setHighlightedItem(contextMenu.lastElementChild, true);
+       break;
+     }
    });

    contextMenu.querySelector("#sa-flag-menu-turbo").addEventListener("click", () => {
      closeContextMenu();
      greenFlag.dispatchEvent(new MouseEvent("click", { bubbles: true, shiftKey: true }));
    });
    contextMenu.querySelector("#sa-flag-menu-fps").addEventListener("click", () => {
      closeContextMenu();
      greenFlag.dispatchEvent(new MouseEvent("click", { bubbles: true, altKey: true }));
    });
    contextMenu.querySelector("#sa-flag-menu-mute").addEventListener("click", () => {
      closeContextMenu();
      greenFlag.dispatchEvent(new MouseEvent("click", { bubbles: true, ctrlKey: true }));
    });

    contextMenu.style.visibility = "hidden"; // Setting visibility here fixes a visual glitch on dynamic enable
    addon.tab.displayNoneWhileDisabled(contextMenu);
    addon.self.addEventListener("disabled", closeContextMenu);

    addon.tab.traps.vm.on("TURBO_MODE_ON", () => {
      contextMenu.querySelector("#sa-flag-menu-turbo").textContent = msg("turbo-off");
    });
    addon.tab.traps.vm.on("TURBO_MODE_OFF", () => {
      contextMenu.querySelector("#sa-flag-menu-turbo").textContent = msg("turbo-on");
    });

    while (true) {
      greenFlag = await addon.tab.waitForElement("[class^='green-flag_green-flag']", {
        markAsSeen: true,
        reduxEvents: ["scratch-gui/mode/SET_PLAYER", "fontsLoaded/SET_FONTS_LOADED", "scratch-gui/locales/SELECT_LOCALE"],
      });

      greenFlag.parentElement.appendChild(contextMenu);
      greenFlag.addEventListener("contextmenu", (e) => {
        if (addon.self.disabled) return;
        e.preventDefault();
+       contextMenuOpen = true;
        contextMenu.classList.add("sa-flag-menu-open");
        contextMenu.style.left = e.clientX + "px";
        contextMenu.style.top = e.clientY + "px";
      });
    }
  }

@DNin01 DNin01 modified the milestones: v1.45.0, v1.46.0 Dec 5, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

new addon Related to new addons to this extension. `scope: addons` should still be added. platform: mobile Related to compatibility with touch screen devices scope: addon api Related to the addon.* JS APIs or other ways for addons to provide features scope: addon Related to one or multiple addons type: enhancement New feature for the project

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Context menu for green flag (to toggle custom framerate, mute, etc.)

3 participants