From 432656862f8926a9f76dcafa034afefebd9d9988 Mon Sep 17 00:00:00 2001 From: Mathias Elle Date: Fri, 6 Nov 2026 12:36:25 +0100 Subject: [PATCH 1/2] =?UTF-8?q?=F0=9F=90=9B=20fix(inspector):=20fix=20pick?= =?UTF-8?q?er=20toggle,=20shortcut=20matching=20and=20alt=20text=20status?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../web/js/inspector/accessibility.js | 19 +++++++++++--- src/view/frontend/web/js/inspector/picker.js | 20 +++++++------- src/view/frontend/web/js/shortcut-parser.js | 26 ++++++++++++++++++- 3 files changed, 51 insertions(+), 14 deletions(-) diff --git a/src/view/frontend/web/js/inspector/accessibility.js b/src/view/frontend/web/js/inspector/accessibility.js index 54773184..ffd56dc3 100644 --- a/src/view/frontend/web/js/inspector/accessibility.js +++ b/src/view/frontend/web/js/inspector/accessibility.js @@ -52,10 +52,16 @@ export const accessibilityMethods = { ); } - // Alt Text (for images) + // Alt Text (for images): false = attribute missing, "" = decorative image if (a11yData.altText !== null) { - const altStatus = a11yData.altText ? a11yData.altText : "⚠️ Missing"; - const altColor = a11yData.altText ? "#34d399" : "#ef4444"; + let altStatus = a11yData.altText; + let altColor = "#34d399"; + if (a11yData.altText === false) { + altStatus = "⚠️ Missing"; + altColor = "#ef4444"; + } else if (a11yData.altText === "") { + altStatus = "Empty (decorative)"; + } container.appendChild( this.createInfoSection("Alt Text", altStatus, altColor), ); @@ -141,9 +147,14 @@ export const accessibilityMethods = { /** * Get alt text for images + * + * @returns {string|false|null} The alt value ("" for decorative images), + * false when an image has no alt attribute, null for non-images. */ getAltText(element, tagName) { - return tagName === "img" ? element.getAttribute("alt") : null; + if (tagName !== "img") return null; + + return element.hasAttribute("alt") ? element.getAttribute("alt") : false; }, /** diff --git a/src/view/frontend/web/js/inspector/picker.js b/src/view/frontend/web/js/inspector/picker.js index 290b56aa..63ecad3b 100644 --- a/src/view/frontend/web/js/inspector/picker.js +++ b/src/view/frontend/web/js/inspector/picker.js @@ -33,16 +33,15 @@ export const pickerMethods = { * Toggle inspector on/off */ toggleInspector() { - this.isOpen = !this.isOpen; - + // Closing also resets a pinned badge, otherwise the next open stays pinned if (this.isOpen) { - this.activatePicker(); - this.$dispatch("mageforge:inspector:opened"); - } else { - this.deactivatePicker(); - this.$dispatch("mageforge:inspector:closed"); + this.closeInspector(); + return; } + this.isOpen = true; + this.activatePicker(); + this.$dispatch("mageforge:inspector:opened"); this.updateFloatingButton(); }, @@ -109,7 +108,7 @@ export const pickerMethods = { if (this.isPinned) return; // Don't update if mouse is over the floating button - if (this.floatingButton && this.floatingButton.contains(e.target)) { + if (this._inspectorFloatButton?.contains(e.target)) { return; } @@ -144,6 +143,8 @@ export const pickerMethods = { if (this.highlightBox) { this.highlightBox.style.display = "none"; } + // Forget the element so hovering it again shows the highlight again + this.hoveredElement = null; // Badge stays visible until hovering another element } }, @@ -157,7 +158,8 @@ export const pickerMethods = { // Check if click is outside badge if ( !this.infoBadge.contains(e.target) && - (!this.floatingButton || !this.floatingButton.contains(e.target)) + (!this._inspectorFloatButton || + !this._inspectorFloatButton.contains(e.target)) ) { this.unpinBadge(); return; diff --git a/src/view/frontend/web/js/shortcut-parser.js b/src/view/frontend/web/js/shortcut-parser.js index fcabc8e0..1ebcf536 100644 --- a/src/view/frontend/web/js/shortcut-parser.js +++ b/src/view/frontend/web/js/shortcut-parser.js @@ -12,6 +12,7 @@ /** * @typedef {object} ParsedShortcut * @property {string} key + * @property {string} code Physical key code (e.g. "KeyI") for single letters and digits, otherwise empty * @property {boolean} ctrlOrMeta * @property {boolean} shift * @property {boolean} alt @@ -26,6 +27,19 @@ */ const parsedShortcutCache = new Map(); +/** + * Map a single letter or digit to its physical KeyboardEvent.code. + * + * @param {string} key Lower-cased key from the shortcut string. + * @returns {string} Empty string for all other keys. + */ +function toKeyCode(key) { + if (/^[a-z]$/.test(key)) return `Key${key.toUpperCase()}`; + if (/^[0-9]$/.test(key)) return `Digit${key}`; + + return ""; +} + /** * Parse a shortcut string into its components. * @@ -48,6 +62,7 @@ export function parseShortcut(shortcut) { parsed = { key, + code: toKeyCode(key), ctrlOrMeta: parts.includes("ctrl") || parts.includes("cmd") || @@ -73,8 +88,17 @@ export function matchesShortcut(event, shortcut) { const parsed = parseShortcut(shortcut); if (!parsed) return false; + // Some events (autofill, synthetic input) carry no key at all + const key = (event.key || "").toLowerCase(); + + // With Alt/Option macOS produces special characters (Option+I => "ˆ"), so + // fall back to the physical key for letters and digits. + const keyMatches = + key === parsed.key || + (parsed.alt && parsed.code !== "" && event.code === parsed.code); + return ( - event.key.toLowerCase() === parsed.key && + keyMatches && (event.ctrlKey || event.metaKey) === parsed.ctrlOrMeta && event.shiftKey === parsed.shift && event.altKey === parsed.alt From b745354e594338c51f0f00f8ef482a88ad5bacd6 Mon Sep 17 00:00:00 2001 From: Mathias Elle Date: Fri, 6 Nov 2026 12:41:40 +0100 Subject: [PATCH 2/2] =?UTF-8?q?=F0=9F=90=9B=20fix(inspector):=20ignore=20k?= =?UTF-8?q?eyless=20events=20and=20trust=20plain=20keys=20in=20shortcut=20?= =?UTF-8?q?matching?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/view/frontend/web/js/shortcut-parser.js | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/src/view/frontend/web/js/shortcut-parser.js b/src/view/frontend/web/js/shortcut-parser.js index 1ebcf536..47dff395 100644 --- a/src/view/frontend/web/js/shortcut-parser.js +++ b/src/view/frontend/web/js/shortcut-parser.js @@ -90,12 +90,19 @@ export function matchesShortcut(event, shortcut) { // Some events (autofill, synthetic input) carry no key at all const key = (event.key || "").toLowerCase(); + if (key === "") return false; - // With Alt/Option macOS produces special characters (Option+I => "ˆ"), so - // fall back to the physical key for letters and digits. + // With Alt/Option macOS produces special characters (Option+I => "ˆ") or + // "Dead", so fall back to the physical key for those. A plain letter or + // digit is trusted as is, otherwise layouts where key and code disagree + // (key "y" on code KeyZ) would match two different shortcuts. + const isPlainKey = /^[a-z0-9]$/.test(key); const keyMatches = key === parsed.key || - (parsed.alt && parsed.code !== "" && event.code === parsed.code); + (parsed.alt && + !isPlainKey && + parsed.code !== "" && + event.code === parsed.code); return ( keyMatches &&