Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 1 addition & 45 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -1292,51 +1292,7 @@ <h2 id="modal-title" data-i18n="about_heading">
id="info-panel"
role="complementary"
aria-label="Information panel"
>
<details id="info-details">
<summary>
<strong data-i18n="keyboard_controls">
Keyboard controls info [Ctrl + /]
</strong>
</summary>

<div id="info-content" class="content" tabindex="-1">
<table role="table" aria-label="Keyboard shortcuts">
<thead class="sr-only">
<tr>
<th scope="col">Shortcut</th>
<th scope="col">Action</th>
</tr>
</thead>
<tbody>
<tr>
<td>Ctrl + M</td>
<td data-i18n="keyboard_menu">Main menu</td>
</tr>
<tr>
<td>Ctrl + P</td>
<td data-i18n="keyboard_play">Play</td>
</tr>
<tr>
<td>Ctrl + G</td>
<td data-i18n="keyboard_gizmos">Gizmos</td>
</tr>
<tr>
<td>Ctrl + E</td>
<td data-i18n="keyboard_workspace">Code editor</td>
</tr>
<tr>
<td>Ctrl + L</td>
<td data-i18n="keyboard_navigation">
Browser navigation bar (overridden shortcuts work from
here)
</td>
</tr>
</tbody>
</table>
</div>
</details>
</aside>
></aside>

<footer id="site-footer">
<nav aria-label="Footer navigation">
Expand Down
189 changes: 189 additions & 0 deletions main/accessibility.js
Original file line number Diff line number Diff line change
Expand Up @@ -251,6 +251,195 @@ const GizmoMenuManager = {
},
};

// Modal showing all keyboard shortcuts, accessed with Ctrl + /

// Check their platform (Mac or not Mac) to show the correct modifier key
function isMac() {
return navigator.platform.toUpperCase().includes("MAC");
}

// List of shortcuts to show in the panel, with categories for grouping
function getShortcuts() {
const mod = isMac() ? "⌘" : "Ctrl";
return [
{ label: "Show/hide shortcut help", keys: `${mod} + /`, category: "Main" },
{
label: "Move between menus, canvas and editor",
keys: `Tab`,
category: "Main",
},
{ label: "Confirm", keys: `Enter`, category: "Main" },
{ label: "Exit", keys: `Esc`, category: "Main" },
{ label: "Play", keys: `${mod} + P`, category: "Main" },
{ label: "Undo", keys: `${mod} + Z`, category: "Main" },
{ label: "Redo", keys: `${mod} + Shift + Z`, category: "Main" },
{
label: "Browser navigation bar (overriden shortcuts work from here)",
keys: `${mod} + L`,
category: "Main",
},

{ label: "Main menu", keys: `${mod} + M`, category: "Menu" },
{ label: "Open file", keys: `${mod} + O`, category: "Menu" },
{ label: "Save / export", keys: `${mod} + S`, category: "Menu" },

{
label: "Open/close area menu",
keys: `${mod} + B`,
category: "Area menu",
},
{ label: "Toggle area", keys: `Tab`, category: "Area menu" },
{ label: "Select area", keys: `1-9 / Enter`, category: "Area menu" },

{ label: "Code editor", keys: `${mod} + E`, category: "Editor" },
{
label: "Add block by name",
keys: `${mod} + ]`,
category: "Editor",
},
{ label: "Search for a block", keys: `${mod} + F`, category: "Editor" },
{ label: "Move through blocks", keys: `↑ ↓ ← →`, category: "Editor" },

{ label: "Gizmos", keys: `${mod} + G`, category: "Gizmos" },
{
label: "Select gizmo",
keys: `1-9`,
category: "Gizmos",
},

{
label: "Keyboard cursor for gizmos",
keys: `↑ ↓ ← →`,
category: "Gizmos",
},
{
label: "Lock transform to axis",
keys: `X Y Z`,
category: "Gizmos",
},
{ label: "Transform in 3D", keys: `↑ ↓ ← → PgUp PgDn`, category: "Gizmos" },
{ label: "Focus camera on object", keys: `F`, category: "Gizmos" },

{
label: "Quick use colour in colour picker",
keys: `P`,
category: "Gizmos",
},
{ label: "Delete object", keys: `Del`, category: "Gizmos" },
];
}

// Formats keys for menu nicely
// You can use + or / and these won't be <kbd> tagged
function formatKeys(keys) {
return keys
.split(/( \+ | \/ )/)
.map((part) =>
part === " + " || part === " / "
? part
: part
.split(" ")
.map((k) => `<kbd>${k}</kbd>`)
.join(" "),
)
.join("");
}

const ShortcutsPanel = {
panel: null,
dock: "left",

init() {
this.createPanel();
this.setupListeners();
},

createPanel() {
const div = document.createElement("div");
div.id = "shortcutsPanel";
div.className = "shortcuts-panel hidden shortcuts-panel--left";
div.setAttribute("role", "region");
div.setAttribute("aria-label", "Keyboard shortcuts");
div.innerHTML = `
<div class="shortcuts-panel__content">
<button type="button" class="close-button" id="closeShortcutsPanel" aria-label="Close keyboard shortcuts">&times;</button>
<h1 id="shortcuts-panel-title">Keyboard shortcuts</h1>
<table id="shortcuts-table"><tbody></tbody></table>
</div>`;
document.body.appendChild(div);
this.panel = div;
},

show() {
const tbody = this.panel.querySelector("tbody");
const groups = getShortcuts().reduce((acc, s) => {
(acc[s.category] ??= []).push(s);
return acc;
}, {});
tbody.innerHTML = Object.entries(groups)
.map(
([cat, items]) => `
<tr><th colspan="2">${cat}</th></tr>
${items.map(({ label, keys }) => `<tr><td>${label}</td><td>${formatKeys(keys)}</td></tr>`).join("")}
`,
)
.join("");
this.panel.classList.remove("hidden");
},

hide() {
this.panel.classList.add("hidden");
},
Comment thread
lawsie marked this conversation as resolved.

toggle() {
this.panel.classList.contains("hidden") ? this.show() : this.hide();
},

setDock(side) {
this.dock = side;
this.panel.classList.toggle("shortcuts-panel--left", side === "left");
this.panel.classList.toggle("shortcuts-panel--right", side === "right");
},

setupListeners() {
document.addEventListener("click", (e) => {
if (e.target.id === "closeShortcutsPanel") this.hide();
});
document.addEventListener("keydown", (e) => {
if (
(e.ctrlKey || e.metaKey) &&
!this.panel.classList.contains("hidden")
) {
const t = e.target;
const tag = (t?.tagName || "").toLowerCase();
if (t?.isContentEditable || tag === "input" || tag === "textarea" || tag === "select") return;

if (e.key === "ArrowLeft") {
e.preventDefault();
this.setDock("left");
}
if (e.key === "ArrowRight") {
e.preventDefault();
this.setDock("right");
}
if (e.key === "ArrowUp") {
e.preventDefault();
const content = this.panel.querySelector(".shortcuts-panel__content");
content.scrollBy({ top: -100, behavior: "instant" });
}
if (e.key === "ArrowDown") {
e.preventDefault();
const content = this.panel.querySelector(".shortcuts-panel__content");
content.scrollBy({ top: 100, behavior: "instant" });
}
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
});
},
};

// Start it up
AreaManager.init();
GizmoMenuManager.init();
ShortcutsPanel.init();

export { ShortcutsPanel };
4 changes: 2 additions & 2 deletions main/blockhandling.js
Original file line number Diff line number Diff line change
Expand Up @@ -292,7 +292,7 @@ export function initializeBlockHandling() {
return;
}

if (event.ctrlKey && event.key === ".") {
if ((event.ctrlKey || event.metaKey) && event.key === ".") {
event.preventDefault();

createKeywordBlockAtViewportCenter("keyword_block");
Expand All @@ -301,7 +301,7 @@ export function initializeBlockHandling() {

// Handle Enter key for adding new blocks
document.addEventListener("keydown", function (event) {
if (event.ctrlKey && event.key === "]") {
if ((event.ctrlKey || event.metaKey) && event.key === "]") {
const selectedBlock = getSelectedBlockForKeywordShortcut();
event.preventDefault();

Expand Down
22 changes: 6 additions & 16 deletions main/input.js
Original file line number Diff line number Diff line change
Expand Up @@ -66,13 +66,6 @@ export function setupInput() {
.querySelectorAll("#gizmoButtons button, #gizmoButtons input")
.forEach(pushUnique);

// 3) Info panel
pushUnique(document.querySelector("#info-details summary"));
const infoDetails = document.getElementById("info-details");
if (infoDetails && infoDetails.open) {
pushUnique(infoDetails.querySelector(".content"));
}

// 4) Logo link + resizer
pushUnique(document.querySelector("#info-panel-link"));
pushUnique(document.querySelector("#resizer"));
Expand Down Expand Up @@ -198,13 +191,6 @@ export function setupInput() {
) {
// Let custom management handle this - will go to next UI element
}
// If we're on the summary and details is open, let browser handle the Tab into content
else if (
activeElement.matches("#info-details summary") &&
detailsElement.open
) {
return; // Let browser handle tab into details content
}
// If we're anywhere inside open details content, let browser handle it
else if (activeElement.closest("#info-details") && detailsElement.open) {
return; // Let browser handle navigation within details
Expand Down Expand Up @@ -302,7 +288,11 @@ export function setupInput() {

function handleCanvasKeyboard(e) {
// Handle Ctrl+Z for undo when canvas is focused
if (e.ctrlKey && e.key.toLowerCase() === "z" && !e.shiftKey) {
if (
(e.ctrlKey || e.metaKey) &&
e.key.toLowerCase() === "z" &&
!e.shiftKey
) {
e.preventDefault();
const workspace = window.mainWorkspace || Blockly.getMainWorkspace();
if (workspace) {
Expand All @@ -314,7 +304,7 @@ export function setupInput() {

// Handle Ctrl+Shift+Z or Ctrl+Y for redo when canvas is focused
if (
(e.ctrlKey && e.shiftKey && e.key.toLowerCase() === "z") ||
((e.ctrlKey || e.metaKey) && e.shiftKey && e.key.toLowerCase() === "z") ||
(e.ctrlKey && e.key.toLowerCase() === "y")
) {
e.preventDefault();
Expand Down
29 changes: 2 additions & 27 deletions main/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ import {
initializeSavedLanguage,
translate,
} from "./translation.js";
import "./accessibility.js";
import { ShortcutsPanel } from "./accessibility.js";

function isEmbedModeEnabled() {
const embedParam = new URLSearchParams(window.location.search).get("embed");
Expand Down Expand Up @@ -577,13 +577,8 @@ function initializeApp() {
break;

case "/": {
// Ctrl+/ - Toggle info details
e.preventDefault();
const infoSummary = document.querySelector("#info-details summary");
if (infoSummary) {
infoSummary.click(); // Simulate a click to toggle details
infoSummary.focus(); // Move focus to the summary
}
ShortcutsPanel.toggle();
break;
}

Expand Down Expand Up @@ -779,26 +774,6 @@ window.onload = async function () {
}
});

const infoDetails = document.getElementById("info-details");
if (infoDetails) {
infoDetails.addEventListener("toggle", function () {
if (this.open) {
setTimeout(() => {
const content = this.querySelector(".content");
if (content) {
content.setAttribute("tabindex", "0"); // Make it focusable
content.focus();
}
}, 10);
} else {
const content = this.querySelector(".content");
if (content) {
content.setAttribute("tabindex", "-1"); // Remove from tab order when closed
}
}
});
}

// Initial view setup
window.loadingCode = true;

Expand Down
Loading
Loading