diff --git a/browser/actors/ScreenshotsComponentChild.sys.mjs b/browser/actors/ScreenshotsComponentChild.sys.mjs index 9cbfa13322c3..9e5148734b38 100644 --- a/browser/actors/ScreenshotsComponentChild.sys.mjs +++ b/browser/actors/ScreenshotsComponentChild.sys.mjs @@ -48,6 +48,11 @@ export class ScreenshotsComponentChild extends JSWindowActorChild { return this.removeEventListeners(); case "Screenshots:AddEventListeners": return this.addEventListeners(); + case "Screenshots:MoveFocusToContent": + return this.focusOverlay(); + case "Screenshots:ClearFocus": + Services.focus.clearFocus(this.contentWindow); + return null; } return null; } @@ -99,8 +104,8 @@ export class ScreenshotsComponentChild extends JSWindowActorChild { this.requestDownloadScreenshot(event.detail.region); break; case "Screenshots:OverlaySelection": { - let { hasSelection } = event.detail; - this.sendOverlaySelection({ hasSelection }); + let { hasSelection, overlayState } = event.detail; + this.sendOverlaySelection({ hasSelection, overlayState }); break; } case "Screenshots:RecordEvent": { @@ -109,10 +114,13 @@ export class ScreenshotsComponentChild extends JSWindowActorChild { break; } case "Screenshots:ShowPanel": - this.showPanel(); + this.sendAsyncMessage("Screenshots:ShowPanel"); break; case "Screenshots:HidePanel": - this.hidePanel(); + this.sendAsyncMessage("Screenshots:HidePanel"); + break; + case "Screenshots:FocusPanel": + this.sendAsyncMessage("Screenshots:MoveFocusToParent", event.detail); break; } } @@ -151,14 +159,6 @@ export class ScreenshotsComponentChild extends JSWindowActorChild { this.endScreenshotsOverlay({ doNotResetMethods: true }); } - showPanel() { - this.sendAsyncMessage("Screenshots:ShowPanel"); - } - - hidePanel() { - this.sendAsyncMessage("Screenshots:HidePanel"); - } - getDocumentTitle() { return this.document.title; } @@ -173,6 +173,11 @@ export class ScreenshotsComponentChild extends JSWindowActorChild { return methodsUsed; } + focusOverlay() { + this.contentWindow.focus(); + this.#overlay.focus(); + } + /** * Resolves when the document is ready to have an overlay injected into it. * diff --git a/browser/components/BrowserGlue.sys.mjs b/browser/components/BrowserGlue.sys.mjs index 7fb320cdb3a0..fcbc9b60e458 100644 --- a/browser/components/BrowserGlue.sys.mjs +++ b/browser/components/BrowserGlue.sys.mjs @@ -732,6 +732,7 @@ let JSWINDOWACTORS = { "Screenshots:OverlaySelection": {}, "Screenshots:RecordEvent": {}, "Screenshots:ShowPanel": {}, + "Screenshots:FocusPanel": {}, }, }, enablePreference: "screenshots.browser.component.enabled", diff --git a/browser/components/screenshots/ScreenshotsOverlayChild.sys.mjs b/browser/components/screenshots/ScreenshotsOverlayChild.sys.mjs index 0f7c1e3fed7c..07ed2bd51778 100644 --- a/browser/components/screenshots/ScreenshotsOverlayChild.sys.mjs +++ b/browser/components/screenshots/ScreenshotsOverlayChild.sys.mjs @@ -133,19 +133,19 @@ export class ScreenshotsOverlay {
-
-
-
-
+
-
+
+
+
+
@@ -305,6 +305,10 @@ export class ScreenshotsOverlay { }; } + focus() { + this.previewCancelButton.focus({ focusVisible: true }); + } + /** * Returns the x and y coordinates of the event relative to both the * viewport and the page. @@ -503,35 +507,51 @@ export class ScreenshotsOverlay { * @param {Event} event The keydown event */ handleKeyDown(event) { - switch (event.key) { - case "ArrowLeft": - this.handleArrowLeftKeyDown(event); + if (event.key === "Escape") { + this.maybeCancelScreenshots(); + return; + } + + switch (this.#state) { + case STATES.CROSSHAIRS: + this.crosshairsKeyDown(event); break; - case "ArrowUp": - this.handleArrowUpKeyDown(event); + case STATES.DRAGGING: + this.draggingKeyDown(event); break; - case "ArrowRight": - this.handleArrowRightKeyDown(event); + case STATES.RESIZING: + this.resizingKeyDown(event); break; - case "ArrowDown": - this.handleArrowDownKeyDown(event); + case STATES.SELECTED: + this.selectedKeyDown(event); break; - case "Tab": - this.maybeLockFocus(event); - break; - case "Escape": - this.maybeCancelScreenshots(); - break; - case this.copyKey.toLowerCase(): - if (this.state === "selected" && this.getAccelKey(event)) { - event.preventDefault(); - this.copySelectedRegion(); - } - break; - case this.downloadKey.toLowerCase(): - if (this.state === "selected" && this.getAccelKey(event)) { - event.preventDefault(); - this.downloadSelectedRegion(); + } + } + + /** + * Handles when a keyup occurs in the screenshots component. + * All we need to do on keyup is set the state to selected. + * @param {Event} event The keydown event + */ + handleKeyUp(event) { + switch (this.#state) { + case STATES.RESIZING: + switch (event.key) { + case "ArrowLeft": + case "ArrowUp": + case "ArrowRight": + case "ArrowDown": + switch (event.originalTarget.id) { + case "highlight": + case "mover-bottomLeft": + case "mover-bottomRight": + case "mover-topLeft": + case "mover-topRight": + event.preventDefault(); + this.#setState(STATES.SELECTED, { doNotMoveFocus: true }); + break; + } + break; } break; } @@ -550,6 +570,175 @@ export class ScreenshotsOverlay { return event.ctrlKey; } + crosshairsKeyDown(event) { + switch (event.key) { + case "ArrowLeft": + case "ArrowUp": + case "ArrowRight": + case "ArrowDown": + // Do nothing so we can prevent default below + break; + case "Tab": + this.maybeLockFocus(event); + return; + case "Enter": + if (this.hoverElementRegion.isRegionValid) { + event.preventDefault(); + this.draggingReadyStart(); + this.draggingReadyDragEnd(); + return; + } + // eslint-disable-next-line no-fallthrough + case " ": { + if (Services.appinfo.isWayland) { + return; + } + + if (event.originalTarget === this.previewCancelButton) { + return; + } + + event.preventDefault(); + // The left and top coordinates from cursorRegion are relative to + // the client window so we need to add the scroll offset of the page to + // get the correct coordinates. + let x = {}; + let y = {}; + this.window.windowUtils.getLastOverWindowPointerLocationInCSSPixels( + x, + y + ); + this.crosshairsDragStart( + x.value + this.windowDimensions.scrollX, + y.value + this.windowDimensions.scrollY + ); + this.#setState(STATES.DRAGGING); + break; + } + default: + return; + } + + // Prevent scrolling with arrow keys + event.preventDefault(); + } + + /** + * Handles a keydown event for the dragging state. + * @param {Event} event The keydown event + */ + draggingKeyDown(event) { + switch (event.key) { + case "ArrowLeft": + this.handleArrowLeftKeyDown(event); + break; + case "ArrowUp": + this.handleArrowUpKeyDown(event); + break; + case "ArrowRight": + this.handleArrowRightKeyDown(event); + break; + case "ArrowDown": + this.handleArrowDownKeyDown(event); + break; + case "Enter": + case " ": + event.preventDefault(); + this.#setState(STATES.SELECTED); + return; + default: + return; + } + + this.drawSelectionContainer(); + } + + /** + * Handles a keydown event for the resizing state. + * @param {Event} event The keydown event + */ + resizingKeyDown(event) { + switch (event.key) { + case "ArrowLeft": + this.resizingArrowLeftKeyDown(event); + break; + case "ArrowUp": + this.resizingArrowUpKeyDown(event); + break; + case "ArrowRight": + this.resizingArrowRightKeyDown(event); + break; + case "ArrowDown": + this.resizingArrowDownKeyDown(event); + break; + } + } + + selectedKeyDown(event) { + let isSelectionElement = event.originalTarget.closest( + "#selection-container" + ); + switch (event.key) { + case "ArrowLeft": + if (isSelectionElement) { + this.resizingArrowLeftKeyDown(event); + } + break; + case "ArrowUp": + if (isSelectionElement) { + this.resizingArrowUpKeyDown(event); + } + break; + case "ArrowRight": + if (isSelectionElement) { + this.resizingArrowRightKeyDown(event); + } + break; + case "ArrowDown": + if (isSelectionElement) { + this.resizingArrowDownKeyDown(event); + } + break; + case "Tab": + this.maybeLockFocus(event); + break; + case " ": + if (!event.originalTarget.closest("#buttons-container")) { + event.preventDefault(); + } + break; + case this.copyKey.toLowerCase(): + if (this.state === "selected" && this.getAccelKey(event)) { + event.preventDefault(); + this.copySelectedRegion(); + } + break; + case this.downloadKey.toLowerCase(): + if (this.state === "selected" && this.getAccelKey(event)) { + event.preventDefault(); + this.downloadSelectedRegion(); + } + break; + } + } + + /** + * Move the region or its left or right side to the left. + * Just the arrow key will move the region by 1px. + * Arrow key + shift will move the region by 10px. + * Arrow key + control/meta will move to the edge of the window. + * @param {Event} event The keydown event + */ + resizingArrowLeftKeyDown(event) { + this.handleArrowLeftKeyDown(event); + + if (this.#state !== STATES.RESIZING) { + this.#setState(STATES.RESIZING); + } + + this.drawSelectionContainer(); + } + /** * Move the region or its left or right side to the left. * Just the arrow key will move the region by 1px. @@ -558,6 +747,7 @@ export class ScreenshotsOverlay { * @param {Event} event The keydown event */ handleArrowLeftKeyDown(event) { + let exponent = event.shiftKey ? 1 : 0; switch (event.originalTarget.id) { case "highlight": if (this.getAccelKey(event)) { @@ -567,7 +757,7 @@ export class ScreenshotsOverlay { break; } - this.selectionRegion.right -= 10 ** event.shiftKey; + this.selectionRegion.right -= 10 ** exponent; // eslint-disable-next-line no-fallthrough case "mover-topLeft": case "mover-bottomLeft": @@ -576,7 +766,7 @@ export class ScreenshotsOverlay { break; } - this.selectionRegion.left -= 10 ** event.shiftKey; + this.selectionRegion.left -= 10 ** exponent; this.scrollIfByEdge( this.selectionRegion.left, this.windowDimensions.scrollY + this.windowDimensions.clientHeight / 2 @@ -596,7 +786,7 @@ export class ScreenshotsOverlay { break; } - this.selectionRegion.right -= 10 ** event.shiftKey; + this.selectionRegion.right -= 10 ** exponent; if (this.selectionRegion.x1 >= this.selectionRegion.x2) { this.selectionRegion.sortCoords(); if (event.originalTarget.id === "mover-topRight") { @@ -610,11 +800,23 @@ export class ScreenshotsOverlay { return; } + event.preventDefault(); + } + + /** + * Move the region or its top or bottom side upward. + * Just the arrow key will move the region by 1px. + * Arrow key + shift will move the region by 10px. + * Arrow key + control/meta will move to the edge of the window. + * @param {Event} event The keydown event + */ + resizingArrowUpKeyDown(event) { + this.handleArrowUpKeyDown(event); + if (this.#state !== STATES.RESIZING) { this.#setState(STATES.RESIZING); } - event.preventDefault(); this.drawSelectionContainer(); } @@ -626,6 +828,7 @@ export class ScreenshotsOverlay { * @param {Event} event The keydown event */ handleArrowUpKeyDown(event) { + let exponent = event.shiftKey ? 1 : 0; switch (event.originalTarget.id) { case "highlight": if (this.getAccelKey(event)) { @@ -635,7 +838,7 @@ export class ScreenshotsOverlay { break; } - this.selectionRegion.bottom -= 10 ** event.shiftKey; + this.selectionRegion.bottom -= 10 ** exponent; // eslint-disable-next-line no-fallthrough case "mover-topLeft": case "mover-topRight": @@ -644,7 +847,7 @@ export class ScreenshotsOverlay { break; } - this.selectionRegion.top -= 10 ** event.shiftKey; + this.selectionRegion.top -= 10 ** exponent; this.scrollIfByEdge( this.windowDimensions.scrollX + this.windowDimensions.clientWidth / 2, this.selectionRegion.top @@ -664,7 +867,7 @@ export class ScreenshotsOverlay { break; } - this.selectionRegion.bottom -= 10 ** event.shiftKey; + this.selectionRegion.bottom -= 10 ** exponent; if (this.selectionRegion.y1 >= this.selectionRegion.y2) { this.selectionRegion.sortCoords(); if (event.originalTarget.id === "mover-bottomLeft") { @@ -678,11 +881,23 @@ export class ScreenshotsOverlay { return; } + event.preventDefault(); + } + + /** + * Move the region or its left or right side to the right. + * Just the arrow key will move the region by 1px. + * Arrow key + shift will move the region by 10px. + * Arrow key + control/meta will move to the edge of the window. + * @param {Event} event The keydown event + */ + resizingArrowRightKeyDown(event) { + this.handleArrowRightKeyDown(event); + if (this.#state !== STATES.RESIZING) { this.#setState(STATES.RESIZING); } - event.preventDefault(); this.drawSelectionContainer(); } @@ -694,6 +909,7 @@ export class ScreenshotsOverlay { * @param {Event} event The keydown event */ handleArrowRightKeyDown(event) { + let exponent = event.shiftKey ? 1 : 0; switch (event.originalTarget.id) { case "highlight": if (this.getAccelKey(event)) { @@ -704,7 +920,7 @@ export class ScreenshotsOverlay { break; } - this.selectionRegion.left += 10 ** event.shiftKey; + this.selectionRegion.left += 10 ** exponent; // eslint-disable-next-line no-fallthrough case "mover-topRight": case "mover-bottomRight": @@ -714,7 +930,7 @@ export class ScreenshotsOverlay { break; } - this.selectionRegion.right += 10 ** event.shiftKey; + this.selectionRegion.right += 10 ** exponent; this.scrollIfByEdge( this.selectionRegion.right, this.windowDimensions.scrollY + this.windowDimensions.clientHeight / 2 @@ -735,7 +951,7 @@ export class ScreenshotsOverlay { break; } - this.selectionRegion.left += 10 ** event.shiftKey; + this.selectionRegion.left += 10 ** exponent; if (this.selectionRegion.x1 >= this.selectionRegion.x2) { this.selectionRegion.sortCoords(); if (event.originalTarget.id === "mover-topLeft") { @@ -749,12 +965,7 @@ export class ScreenshotsOverlay { return; } - if (this.#state !== STATES.RESIZING) { - this.#setState(STATES.RESIZING); - } - event.preventDefault(); - this.drawSelectionContainer(); } /** @@ -764,7 +975,18 @@ export class ScreenshotsOverlay { * Arrow key + control/meta will move to the edge of the window. * @param {Event} event The keydown event */ + resizingArrowDownKeyDown(event) { + this.handleArrowDownKeyDown(event); + + if (this.#state !== STATES.RESIZING) { + this.#setState(STATES.RESIZING); + } + + this.drawSelectionContainer(); + } + handleArrowDownKeyDown(event) { + let exponent = event.shiftKey ? 1 : 0; switch (event.originalTarget.id) { case "highlight": if (this.getAccelKey(event)) { @@ -775,7 +997,7 @@ export class ScreenshotsOverlay { break; } - this.selectionRegion.top += 10 ** event.shiftKey; + this.selectionRegion.top += 10 ** exponent; // eslint-disable-next-line no-fallthrough case "mover-bottomLeft": case "mover-bottomRight": @@ -785,7 +1007,7 @@ export class ScreenshotsOverlay { break; } - this.selectionRegion.bottom += 10 ** event.shiftKey; + this.selectionRegion.bottom += 10 ** exponent; this.scrollIfByEdge( this.windowDimensions.scrollX + this.windowDimensions.clientWidth / 2, this.selectionRegion.bottom @@ -806,7 +1028,7 @@ export class ScreenshotsOverlay { break; } - this.selectionRegion.top += 10 ** event.shiftKey; + this.selectionRegion.top += 10 ** exponent; if (this.selectionRegion.y1 >= this.selectionRegion.y2) { this.selectionRegion.sortCoords(); if (event.originalTarget.id === "mover-topLeft") { @@ -820,12 +1042,7 @@ export class ScreenshotsOverlay { return; } - if (this.#state !== STATES.RESIZING) { - this.#setState(STATES.RESIZING); - } - event.preventDefault(); - this.drawSelectionContainer(); } /** @@ -834,27 +1051,39 @@ export class ScreenshotsOverlay { * @param {Event} event The keydown event */ maybeLockFocus(event) { - if (this.#state !== STATES.SELECTED) { - return; - } - event.preventDefault(); - if (event.originalTarget.id === "highlight" && event.shiftKey) { - this.downloadButton.focus({ focusVisible: true }); - } else if (event.originalTarget.id === "download" && !event.shiftKey) { - this.highlightEl.focus({ focusVisible: true }); - } else { - // The content document can listen for keydown events and prevent moving - // focus so we manually move focus to the next element here. - let direction = event.shiftKey - ? Services.focus.MOVEFOCUS_BACKWARD - : Services.focus.MOVEFOCUS_FORWARD; - Services.focus.moveFocus( - this.window, - null, - direction, - Services.focus.FLAG_BYKEY - ); + + switch (this.#state) { + case STATES.CROSSHAIRS: + if (event.shiftKey) { + this.#dispatchEvent("Screenshots:FocusPanel", { + direction: "backward", + }); + } else { + this.#dispatchEvent("Screenshots:FocusPanel", { + direction: "forward", + }); + } + break; + case STATES.SELECTED: + if (event.originalTarget.id === "highlight" && event.shiftKey) { + this.downloadButton.focus({ focusVisible: true }); + } else if (event.originalTarget.id === "download" && !event.shiftKey) { + this.highlightEl.focus({ focusVisible: true }); + } else { + // The content document can listen for keydown events and prevent moving + // focus so we manually move focus to the next element here. + let direction = event.shiftKey + ? Services.focus.MOVEFOCUS_BACKWARD + : Services.focus.MOVEFOCUS_FORWARD; + Services.focus.moveFocus( + this.window, + null, + direction, + Services.focus.FLAG_BYKEY + ); + } + break; } } @@ -870,31 +1099,6 @@ export class ScreenshotsOverlay { } } - /** - * Handles when a keydown occurs in the screenshots component. - * All we need to do on keyup is set the state to selected. - * @param {Event} event The keydown event - */ - handleKeyUp(event) { - switch (event.key) { - case "ArrowLeft": - case "ArrowUp": - case "ArrowRight": - case "ArrowDown": - switch (event.originalTarget.id) { - case "highlight": - case "mover-bottomLeft": - case "mover-bottomRight": - case "mover-topLeft": - case "mover-topRight": - event.preventDefault(); - this.#setState(STATES.SELECTED); - break; - } - break; - } - } - /** * All of the selection ranges were recorded at initialization. The ranges * are removed when focus is set to the buttons so we add the selection @@ -926,8 +1130,9 @@ export class ScreenshotsOverlay { /** * Set a new state for the overlay * @param {String} newState + * @param {Object} options (optional) Options for calling start of state method */ - #setState(newState) { + #setState(newState, options = {}) { if (this.#state === STATES.SELECTED && newState === STATES.CROSSHAIRS) { this.#dispatchEvent("Screenshots:RecordEvent", { eventName: "started", @@ -936,7 +1141,13 @@ export class ScreenshotsOverlay { } if (newState !== this.#state) { this.#dispatchEvent("Screenshots:OverlaySelection", { - hasSelection: newState == STATES.SELECTED, + hasSelection: [ + STATES.DRAGGING_READY, + STATES.DRAGGING, + STATES.RESIZING, + STATES.SELECTED, + ].includes(newState), + overlayState: newState, }); } this.#state = newState; @@ -955,7 +1166,7 @@ export class ScreenshotsOverlay { break; } case STATES.SELECTED: { - this.selectedStart(); + this.selectedStart(options); break; } case STATES.RESIZING: { @@ -1015,11 +1226,16 @@ export class ScreenshotsOverlay { * Hide the preview and hover element containers. * Draw the selection and buttons containers. */ - selectedStart() { + selectedStart(options) { + this.selectionRegion.sortCoords(); this.hidePreviewContainer(); this.hideHoverElementContainer(); this.drawSelectionContainer(); this.drawButtonsContainer(); + + if (!options.doNotMoveFocus) { + this.setFocusToActionButton(); + } } /** @@ -1246,7 +1462,6 @@ export class ScreenshotsOverlay { if (this.hoverElementRegion.isRegionValid) { this.selectionRegion.dimensions = this.hoverElementRegion.dimensions; this.#setState(STATES.SELECTED); - this.setFocusToActionButton(); this.#dispatchEvent("Screenshots:RecordEvent", { eventName: "selected", reason: "element", @@ -1267,11 +1482,9 @@ export class ScreenshotsOverlay { right: pageX, bottom: pageY, }; - this.selectionRegion.sortCoords(); this.#setState(STATES.SELECTED); this.maybeRecordRegionSelected(); this.#methodsUsed.region += 1; - this.setFocusToActionButton(); } /** @@ -1282,9 +1495,7 @@ export class ScreenshotsOverlay { */ resizingDragEnd(pageX, pageY) { this.resizingDrag(pageX, pageY); - this.selectionRegion.sortCoords(); this.#setState(STATES.SELECTED); - this.setFocusToActionButton(); this.maybeRecordRegionSelected(); if (this.#moverId === "highlight") { this.#methodsUsed.move += 1; @@ -1491,6 +1702,10 @@ export class ScreenshotsOverlay { this.buttonsContainer.hidden = true; } + updateCursorRegion(left, top) { + this.cursorRegion = { left, top, right: left, bottom: top }; + } + /** * Set the pointer events to none on the screenshots elements so * elementFromPoint can find the real element at the given point. diff --git a/browser/components/screenshots/ScreenshotsUtils.sys.mjs b/browser/components/screenshots/ScreenshotsUtils.sys.mjs index 6e19194c4036..476126b412be 100644 --- a/browser/components/screenshots/ScreenshotsUtils.sys.mjs +++ b/browser/components/screenshots/ScreenshotsUtils.sys.mjs @@ -91,6 +91,7 @@ export class ScreenshotsComponentParent extends JSWindowActorParent { case "Screenshots:OverlaySelection": ScreenshotsUtils.setPerBrowserState(browser, { hasOverlaySelection: message.data.hasSelection, + overlayState: message.data.overlayState, }); break; case "Screenshots:ShowPanel": @@ -99,6 +100,9 @@ export class ScreenshotsComponentParent extends JSWindowActorParent { case "Screenshots:HidePanel": ScreenshotsUtils.closePanel(browser); break; + case "Screenshots:MoveFocusToParent": + ScreenshotsUtils.focusPanel(browser, message.data); + break; } } @@ -191,11 +195,7 @@ export var ScreenshotsUtils = { handleEvent(event) { switch (event.type) { case "keydown": - if (event.key === "Escape") { - // Escape should cancel and exit - let browser = event.view.gBrowser.selectedBrowser; - this.cancel(browser, "escape"); - } + this.handleKeyDownEvent(event); break; case "TabSelect": this.handleTabSelect(event); @@ -209,6 +209,33 @@ export var ScreenshotsUtils = { } }, + handleKeyDownEvent(event) { + let browser = + event.view.browsingContext.topChromeWindow.gBrowser.selectedBrowser; + if (!browser) { + return; + } + + switch (event.key) { + case "Escape": + // The chromeEventHandler in the child actor will handle events that + // don't match this + if (event.target.parentElement === this.panelForBrowser(browser)) { + this.cancel(browser, "escape"); + } + break; + case "ArrowLeft": + case "ArrowUp": + case "ArrowRight": + case "ArrowDown": + this.handleArrowKeyDown(event, browser); + break; + case "Tab": + this.maybeLockFocus(event); + break; + } + }, + /** * When we swap docshells for a given screenshots browser, we need to update * the browserToScreenshotsState WeakMap to the correct browser. If the old @@ -273,6 +300,104 @@ export var ScreenshotsUtils = { } }, + /** + * If the overlay state is crosshairs or dragging, move the native cursor + * respective to the arrow key pressed. + * @param {Event} event A keydown event + * @param {Browser} browser The selected browser + * @returns + */ + handleArrowKeyDown(event, browser) { + if (Services.appinfo.isWayland) { + return; + } + + let { overlayState } = this.browserToScreenshotsState.get(browser); + + if (!["crosshairs", "dragging"].includes(overlayState)) { + return; + } + + let left = 0; + let top = 0; + let exponent = event.shiftKey ? 1 : 0; + switch (event.key) { + case "ArrowLeft": + left -= 10 ** exponent; + break; + case "ArrowUp": + top -= 10 ** exponent; + break; + case "ArrowRight": + left += 10 ** exponent; + break; + case "ArrowDown": + top += 10 ** exponent; + break; + default: + return; + } + + // Clear and move focus to browser so the child actor can capture events + this.clearContentFocus(browser); + Services.focus.clearFocus(browser.ownerGlobal); + Services.focus.setFocus(browser, 0); + + let x = {}; + let y = {}; + let win = browser.ownerGlobal; + win.windowUtils.getLastOverWindowPointerLocationInCSSPixels(x, y); + + this.moveCursor( + { + left: (x.value + left) * win.devicePixelRatio, + top: (y.value + top) * win.devicePixelRatio, + }, + browser + ); + }, + + /** + * Move the native cursor to the given position. Clamp the position to the + * window just in case. + * @param {Object} position An object containing the left and top position + * @param {Browser} browser The selected browser + */ + moveCursor(position, browser) { + let { left, top } = position; + let win = browser.ownerGlobal; + + const windowLeft = win.mozInnerScreenX * win.devicePixelRatio; + const windowTop = win.mozInnerScreenY * win.devicePixelRatio; + const contentTop = + (win.mozInnerScreenY + (win.innerHeight - browser.clientHeight)) * + win.devicePixelRatio; + const windowRight = + (win.mozInnerScreenX + win.innerWidth) * win.devicePixelRatio; + const windowBottom = + (win.mozInnerScreenY + win.innerHeight) * win.devicePixelRatio; + + left += windowLeft; + top += windowTop; + + // Clamp left and top to content dimensions + let parsedLeft = Math.round( + Math.min(Math.max(left, windowLeft), windowRight) + ); + let parsedTop = Math.round( + Math.min(Math.max(top, contentTop), windowBottom) + ); + + win.windowUtils.sendNativeMouseEvent( + parsedLeft, + parsedTop, + win.windowUtils.NATIVE_MOUSE_MESSAGE_MOVE, + 0, + 0, + win.document.documentElement + ); + }, + observe(subj, topic, data) { let { gBrowser } = subj; let browser = gBrowser.selectedBrowser; @@ -335,6 +460,12 @@ export var ScreenshotsUtils = { browser.addEventListener("SwapDocShells", this); let gBrowser = browser.getTabBrowser(); gBrowser.tabContainer.addEventListener("TabSelect", this); + + // Wayland doesn't support `sendNativeMouseEvent` which is what the + // keydown event is used for. + if (!Services.appinfo.isWayland) { + browser.addEventListener("keydown", this); + } break; } case UIPhases.INITIAL: @@ -364,6 +495,9 @@ export var ScreenshotsUtils = { browser.removeEventListener("SwapDocShells", this); const gBrowser = browser.getTabBrowser(); gBrowser.tabContainer.removeEventListener("TabSelect", this); + if (!Services.appinfo.isWayland) { + browser.removeEventListener("keydown", this); + } this.browserToScreenshotsState.delete(browser); if (Cu.isInAutomation) { @@ -396,6 +530,53 @@ export var ScreenshotsUtils = { Object.assign(perBrowserState, nameValues); }, + maybeLockFocus(event) { + let browser = event.view.gBrowser.selectedBrowser; + + if (!Services.focus.focusedElement) { + event.preventDefault(); + this.focusPanel(browser); + return; + } + + let target = event.explicitOriginalTarget; + + if (!target.closest("moz-button-group")) { + return; + } + + let isElementFirst = !!target.nextElementSibling; + + if ( + (isElementFirst && event.shiftKey) || + (!isElementFirst && !event.shiftKey) + ) { + event.preventDefault(); + this.moveFocusToContent(browser); + } + }, + + focusPanel(browser, { direction } = {}) { + let buttonsPanel = this.panelForBrowser(browser); + if (direction) { + buttonsPanel + .querySelector("screenshots-buttons") + .focusButton(direction === "forward" ? "first" : "last"); + } else { + buttonsPanel + .querySelector("screenshots-buttons") + .focusButton(lazy.SCREENSHOTS_LAST_SCREENSHOT_METHOD); + } + }, + + moveFocusToContent(browser) { + this.getActor(browser).sendAsyncMessage("Screenshots:MoveFocusToContent"); + }, + + clearContentFocus(browser) { + this.getActor(browser).sendAsyncMessage("Screenshots:ClearFocus"); + }, + /** * Attempt to place focus on the element that had focus before screenshots UI was shown * diff --git a/browser/components/screenshots/screenshots-buttons.css b/browser/components/screenshots/screenshots-buttons.css index b63308d8b4e0..ccb092174ed6 100644 --- a/browser/components/screenshots/screenshots-buttons.css +++ b/browser/components/screenshots/screenshots-buttons.css @@ -14,15 +14,15 @@ border-radius: var(--arrowpanel-border-radius); } -.full-page { +#full-page { background-image: url("chrome://browser/content/screenshots/menu-fullpage.svg"); } -.visible-page { +#visible-page { background-image: url("chrome://browser/content/screenshots/menu-visible.svg"); } -.full-page, .visible-page { +#full-page, #visible-page { -moz-context-properties: fill, stroke; fill: currentColor; /* stroke is the secondary fill color used to define the viewport shape in the SVGs */ diff --git a/browser/components/screenshots/screenshots-buttons.js b/browser/components/screenshots/screenshots-buttons.js index 9ac8dab2cf4b..e501da5a51bc 100644 --- a/browser/components/screenshots/screenshots-buttons.js +++ b/browser/components/screenshots/screenshots-buttons.js @@ -20,9 +20,10 @@ - - + + + `; } @@ -41,12 +42,12 @@ this.shadowRoot.append(ScreenshotsButtons.fragment); - let visibleButton = this.shadowRoot.querySelector(".visible-page"); + let visibleButton = shadowRoot.getElementById("visible-page"); visibleButton.onclick = function () { ScreenshotsUtils.doScreenshot(gBrowser.selectedBrowser, "visible"); }; - let fullpageButton = this.shadowRoot.querySelector(".full-page"); + let fullpageButton = shadowRoot.getElementById("full-page"); fullpageButton.onclick = function () { ScreenshotsUtils.doScreenshot(gBrowser.selectedBrowser, "full_page"); }; @@ -65,11 +66,19 @@ await this.shadowRoot.querySelector("moz-button-group").updateComplete; if (buttonToFocus === "fullpage") { this.shadowRoot - .querySelector(".full-page") + .getElementById("full-page") .focus({ focusVisible: true }); + } else if (buttonToFocus === "first") { + this.shadowRoot + .querySelector("moz-button-group") + .firstElementChild.focus({ focusVisible: true }); + } else if (buttonToFocus === "last") { + this.shadowRoot + .querySelector("moz-button-group") + .lastElementChild.focus({ focusVisible: true }); } else { this.shadowRoot - .querySelector(".visible-page") + .getElementById("visible-page") .focus({ focusVisible: true }); } } diff --git a/browser/components/screenshots/tests/browser/browser.toml b/browser/components/screenshots/tests/browser/browser.toml index 976a0037c805..5923088b3146 100644 --- a/browser/components/screenshots/tests/browser/browser.toml +++ b/browser/components/screenshots/tests/browser/browser.toml @@ -21,6 +21,8 @@ skip-if = ["os == 'linux'"] ["browser_keyboard_shortcuts.js"] +["browser_keyboard_tests.js"] + ["browser_overlay_keyboard_test.js"] ["browser_screenshots_drag_scroll_test.js"] diff --git a/browser/components/screenshots/tests/browser/browser_keyboard_shortcuts.js b/browser/components/screenshots/tests/browser/browser_keyboard_shortcuts.js index bca96f333f51..66ab25f1c6cc 100644 --- a/browser/components/screenshots/tests/browser/browser_keyboard_shortcuts.js +++ b/browser/components/screenshots/tests/browser/browser_keyboard_shortcuts.js @@ -56,7 +56,7 @@ add_task(async function test_download_shortcut() { "screenshots-preview-ready" ); - let visibleButton = await helper.getPanelButton(".visible-page"); + let visibleButton = await helper.getPanelButton("#visible-page"); visibleButton.click(); await screenshotReady; @@ -108,7 +108,7 @@ add_task(async function test_copy_shortcut() { "screenshots-preview-ready" ); - let visibleButton = await helper.getPanelButton(".visible-page"); + let visibleButton = await helper.getPanelButton("#visible-page"); visibleButton.click(); await screenshotReady; diff --git a/browser/components/screenshots/tests/browser/browser_keyboard_tests.js b/browser/components/screenshots/tests/browser/browser_keyboard_tests.js new file mode 100644 index 000000000000..fa75f7d13397 --- /dev/null +++ b/browser/components/screenshots/tests/browser/browser_keyboard_tests.js @@ -0,0 +1,489 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const KEY_TO_EXPECTED_POSITION_ARRAY = [ + [ + "ArrowRight", + { + top: 100, + left: 100, + bottom: 100, + right: 110, + }, + ], + [ + "ArrowDown", + { + top: 100, + left: 100, + bottom: 110, + right: 110, + }, + ], + [ + "ArrowLeft", + { + top: 100, + left: 100, + bottom: 110, + right: 100, + }, + ], + [ + "ArrowUp", + { + top: 100, + left: 100, + bottom: 100, + right: 100, + }, + ], + ["ArrowDown", { top: 100, left: 100, bottom: 110, right: 100 }], + [ + "ArrowRight", + { + top: 100, + left: 100, + bottom: 110, + right: 110, + }, + ], + [ + "ArrowUp", + { + top: 100, + left: 100, + bottom: 100, + right: 110, + }, + ], + [ + "ArrowLeft", + { + top: 100, + left: 100, + bottom: 100, + right: 100, + }, + ], +]; + +const SHIFT_PLUS_KEY_TO_EXPECTED_POSITION_ARRAY = [ + [ + "ArrowRight", + { + top: 100, + left: 100, + bottom: 100, + right: 200, + }, + ], + [ + "ArrowDown", + { + top: 100, + left: 100, + bottom: 200, + right: 200, + }, + ], + [ + "ArrowLeft", + { + top: 100, + left: 100, + bottom: 200, + right: 100, + }, + ], + [ + "ArrowUp", + { + top: 100, + left: 100, + bottom: 100, + right: 100, + }, + ], + ["ArrowDown", { top: 100, left: 100, bottom: 200, right: 100 }], + [ + "ArrowRight", + { + top: 100, + left: 100, + bottom: 200, + right: 200, + }, + ], + [ + "ArrowUp", + { + top: 100, + left: 100, + bottom: 100, + right: 200, + }, + ], + [ + "ArrowLeft", + { + top: 100, + left: 100, + bottom: 100, + right: 100, + }, + ], +]; + +add_task(async function test_focusMovesToContentOnArrowKeydown() { + await BrowserTestUtils.withNewTab( + { + gBrowser, + url: TEST_PAGE, + }, + async browser => { + let helper = new ScreenshotsHelper(browser); + helper.triggerUIFromToolbar(); + await helper.waitForOverlay(); + + let visibleButton = await helper.getPanelButton("#visible-page"); + visibleButton.focus(); + + await BrowserTestUtils.waitForCondition(() => { + return visibleButton.getRootNode().activeElement === visibleButton; + }, "The visible button in the panel should have focus"); + info( + `Actual focused id: ${Services.focus.focusedElement.id}. Expected focused id: ${visibleButton.id}` + ); + is( + Services.focus.focusedElement, + visibleButton, + "The visible button in the panel should have focus" + ); + + EventUtils.synthesizeKey("ArrowLeft"); + + // Focus should move to the content document + let fullpageButton = await helper.getPanelButton("#full-page"); + await BrowserTestUtils.waitForCondition(() => { + return ( + fullpageButton.getRootNode().activeElement !== fullpageButton && + visibleButton.getRootNode().activeElement !== visibleButton + ); + }, "The visible and full page buttons do not have focus"); + Assert.notEqual( + Services.focus.focusedElement, + visibleButton, + "The visible button does not have focus" + ); + Assert.notEqual( + Services.focus.focusedElement, + fullpageButton, + "The full page button does not have focus" + ); + + await ContentTask.spawn(browser, null, async () => { + let screenshotsChild = content.windowGlobalChild.getActor( + "ScreenshotsComponent" + ); + + let { left: currentCursorX, top: currentCursorY } = + screenshotsChild.overlay.cursorRegion; + + let rect = content.document + .getElementById("testPageElement") + .getBoundingClientRect(); + + let repeatShiftLeft = Math.round((currentCursorX - rect.right) / 10); + EventUtils.synthesizeKey( + "ArrowLeft", + { repeat: repeatShiftLeft, shiftKey: true }, + content + ); + + let repeatLeft = (currentCursorX - rect.right) % 10; + EventUtils.synthesizeKey("ArrowLeft", { repeat: repeatLeft }, content); + + let repeatShiftRight = Math.round((currentCursorY - rect.bottom) / 10); + EventUtils.synthesizeKey( + "ArrowUp", + { repeat: repeatShiftRight, shiftKey: true }, + content + ); + + let repeatRight = (currentCursorY - rect.bottom) % 10; + EventUtils.synthesizeKey("ArrowUp", { repeat: repeatRight }, content); + + await ContentTaskUtils.waitForCondition(() => { + return screenshotsChild.overlay.hoverElementRegion.isRegionValid; + }, "Wait for hover element region to be valid"); + + EventUtils.synthesizeKey("Enter", {}, content); + }); + + let rect = await helper.getTestPageElementRect(); + let region = await helper.getSelectionRegionDimensions(); + + is( + region.left, + rect.left, + "The selected region left is the same as the element left" + ); + is( + region.right, + rect.right, + "The selected region right is the same as the element right" + ); + is( + region.top, + rect.top, + "The selected region top is the same as the element top" + ); + is( + region.bottom, + rect.bottom, + "The selected region bottom is the same as the element bottom" + ); + } + ); +}); + +add_task(async function test_createRegionWithKeyboard() { + await BrowserTestUtils.withNewTab( + { + gBrowser, + url: TEST_PAGE, + }, + async browser => { + let helper = new ScreenshotsHelper(browser); + + helper.triggerUIFromToolbar(); + await helper.waitForOverlay(); + + mouse.move(100, 100); + + await SpecialPowers.spawn( + browser, + [KEY_TO_EXPECTED_POSITION_ARRAY], + async keyToExpectedPositionArray => { + function assertSelectionRegionDimensions( + actualDimensions, + expectedDimensions + ) { + is( + actualDimensions.top, + expectedDimensions.top, + "Top dimension is correct" + ); + is( + actualDimensions.left, + expectedDimensions.left, + "Left dimension is correct" + ); + is( + actualDimensions.bottom, + expectedDimensions.bottom, + "Bottom dimension is correct" + ); + is( + actualDimensions.right, + expectedDimensions.right, + "Right dimension is correct" + ); + } + + let stateChangePromise = expectedState => + ContentTaskUtils.waitForCondition(() => { + info( + `got ${screenshotsChild.overlay.state}. expected ${expectedState}` + ); + return screenshotsChild.overlay.state === expectedState; + }, `Wait for overlay state to be ${expectedState}`); + + let screenshotsChild = content.windowGlobalChild.getActor( + "ScreenshotsComponent" + ); + + await ContentTaskUtils.waitForCondition(() => { + return ( + screenshotsChild.overlay.cursorRegion.left === 100 && + screenshotsChild.overlay.cursorRegion.top === 100 + ); + }, "Wait for cursor region to be at 10, 10"); + + EventUtils.synthesizeKey(" ", {}, content); + + let expectedState = "dragging"; + await stateChangePromise(expectedState); + + for (let [key, expectedDimensions] of keyToExpectedPositionArray) { + EventUtils.synthesizeKey(key, { repeat: 10 }, content); + info(`Key: ${key}`); + info( + `Actual dimensions: ${JSON.stringify( + screenshotsChild.overlay.selectionRegion.dimensions, + null, + 2 + )}` + ); + info( + `Expected dimensions: ${JSON.stringify( + expectedDimensions, + null, + 2 + )}` + ); + assertSelectionRegionDimensions( + screenshotsChild.overlay.selectionRegion.dimensions, + expectedDimensions + ); + } + + EventUtils.synthesizeKey("ArrowRight", { repeat: 10 }, content); + EventUtils.synthesizeKey("ArrowDown", { repeat: 10 }, content); + + EventUtils.synthesizeKey(" ", {}, content); + expectedState = "selected"; + await stateChangePromise(expectedState); + } + ); + + let region = await helper.getSelectionRegionDimensions(); + + is(region.left, 100, "The selected region left is 100"); + is(region.right, 110, "The selected region right is 110"); + is(region.top, 100, "The selected region top is 100"); + is(region.bottom, 110, "The selected region bottom is 110"); + + helper.triggerUIFromToolbar(); + await helper.waitForOverlayClosed(); + } + ); +}); + +add_task(async function test_createRegionWithKeyboardWithShift() { + await BrowserTestUtils.withNewTab( + { + gBrowser, + url: TEST_PAGE, + }, + async browser => { + let helper = new ScreenshotsHelper(browser); + + helper.triggerUIFromToolbar(); + await helper.waitForOverlay(); + + mouse.move(100, 100); + + await SpecialPowers.spawn( + browser, + [SHIFT_PLUS_KEY_TO_EXPECTED_POSITION_ARRAY], + async keyToExpectedPositionArray => { + function assertSelectionRegionDimensions( + actualDimensions, + expectedDimensions + ) { + is( + actualDimensions.top, + expectedDimensions.top, + "Top dimension is correct" + ); + is( + actualDimensions.left, + expectedDimensions.left, + "Left dimension is correct" + ); + is( + actualDimensions.bottom, + expectedDimensions.bottom, + "Bottom dimension is correct" + ); + is( + actualDimensions.right, + expectedDimensions.right, + "Right dimension is correct" + ); + } + + let stateChangePromise = expectedState => + ContentTaskUtils.waitForCondition(() => { + info( + `got ${screenshotsChild.overlay.state}. expected ${expectedState}` + ); + return screenshotsChild.overlay.state === expectedState; + }, `Wait for overlay state to be ${expectedState}`); + + let screenshotsChild = content.windowGlobalChild.getActor( + "ScreenshotsComponent" + ); + + await ContentTaskUtils.waitForCondition(() => { + return ( + screenshotsChild.overlay.cursorRegion.left === 100 && + screenshotsChild.overlay.cursorRegion.top === 100 + ); + }, "Wait for cursor region to be at 10, 10"); + + EventUtils.synthesizeKey(" ", {}, content); + + let expectedState = "dragging"; + await stateChangePromise(expectedState); + + for (let [key, expectedDimensions] of keyToExpectedPositionArray) { + EventUtils.synthesizeKey( + key, + { repeat: 10, shiftKey: true }, + content + ); + info(`Key: ${key}`); + info( + `Actual dimensions: ${JSON.stringify( + screenshotsChild.overlay.selectionRegion.dimensions, + null, + 2 + )}` + ); + info( + `Expected dimensions: ${JSON.stringify( + expectedDimensions, + null, + 2 + )}` + ); + assertSelectionRegionDimensions( + screenshotsChild.overlay.selectionRegion.dimensions, + expectedDimensions + ); + } + + EventUtils.synthesizeKey( + "ArrowRight", + { repeat: 10, shiftKey: true }, + content + ); + EventUtils.synthesizeKey( + "ArrowDown", + { repeat: 10, shiftKey: true }, + content + ); + + EventUtils.synthesizeKey(" ", {}, content); + expectedState = "selected"; + await stateChangePromise(expectedState); + } + ); + + let region = await helper.getSelectionRegionDimensions(); + + is(region.left, 100, "The selected region left is 100"); + is(region.right, 200, "The selected region right is 200"); + is(region.top, 100, "The selected region top is 100"); + is(region.bottom, 200, "The selected region bottom is 200"); + + helper.triggerUIFromToolbar(); + await helper.waitForOverlayClosed(); + } + ); +}); diff --git a/browser/components/screenshots/tests/browser/browser_overlay_keyboard_test.js b/browser/components/screenshots/tests/browser/browser_overlay_keyboard_test.js index 592587a67d7d..71b93b5c0656 100644 --- a/browser/components/screenshots/tests/browser/browser_overlay_keyboard_test.js +++ b/browser/components/screenshots/tests/browser/browser_overlay_keyboard_test.js @@ -136,9 +136,6 @@ const SHIFT_PLUS_KEY_TO_EXPECTED_POSITION_ARRAY = [ ], ]; -/** - * - */ add_task(async function test_moveRegionWithKeyboard() { await BrowserTestUtils.withNewTab( { diff --git a/browser/components/screenshots/tests/browser/browser_screenshots_focus_test.js b/browser/components/screenshots/tests/browser/browser_screenshots_focus_test.js index e2b412dc1c90..c8e3142c601a 100644 --- a/browser/components/screenshots/tests/browser/browser_screenshots_focus_test.js +++ b/browser/components/screenshots/tests/browser/browser_screenshots_focus_test.js @@ -46,7 +46,7 @@ async function restoreFocusOnEscape(initialFocusElem, helper) { ); EventUtils.synthesizeKey("s", { shiftKey: true, accelKey: true }); - let button = await helper.getPanelButton(".visible-page"); + let button = await helper.getPanelButton("#visible-page"); info("Panel is now visible, got button: " + button.className); info( `focusedElement: ${Services.focus.focusedElement.localName}.${Services.focus.focusedElement.className}` @@ -88,7 +88,7 @@ add_task(async function testPanelFocused() { info("Opening Screenshots and waiting for the panel"); helper.triggerUIFromToolbar(); - let button = await helper.getPanelButton(".visible-page"); + let button = await helper.getPanelButton("#visible-page"); info("Panel is now visible, got button: " + button.className); info( `focusedElement: ${Services.focus.focusedElement.localName}.${Services.focus.focusedElement.className}` @@ -215,7 +215,7 @@ add_task(async function test_focusLastUsedMethod() { helper.triggerUIFromToolbar(); await helper.waitForOverlay(); - let expectedFocusedButton = await helper.getPanelButton(".visible-page"); + let expectedFocusedButton = await helper.getPanelButton("#visible-page"); await BrowserTestUtils.waitForCondition(() => { return ( @@ -233,7 +233,7 @@ add_task(async function test_focusLastUsedMethod() { let screenshotReady = TestUtils.topicObserved( "screenshots-preview-ready" ); - let fullpageButton = await helper.getPanelButton(".full-page"); + let fullpageButton = await helper.getPanelButton("#full-page"); fullpageButton.click(); await screenshotReady; @@ -242,7 +242,7 @@ add_task(async function test_focusLastUsedMethod() { await helper.waitForOverlay(); - expectedFocusedButton = await helper.getPanelButton(".full-page"); + expectedFocusedButton = await helper.getPanelButton("#full-page"); await BrowserTestUtils.waitForCondition(() => { return ( @@ -258,7 +258,7 @@ add_task(async function test_focusLastUsedMethod() { ); screenshotReady = TestUtils.topicObserved("screenshots-preview-ready"); - let visiblepageButton = await helper.getPanelButton(".visible-page"); + let visiblepageButton = await helper.getPanelButton("#visible-page"); visiblepageButton.click(); await screenshotReady; @@ -267,7 +267,7 @@ add_task(async function test_focusLastUsedMethod() { await helper.waitForOverlay(); - expectedFocusedButton = await helper.getPanelButton(".visible-page"); + expectedFocusedButton = await helper.getPanelButton("#visible-page"); await BrowserTestUtils.waitForCondition(() => { return ( @@ -309,7 +309,7 @@ add_task(async function test_focusLastUsedMethod() { helper.triggerUIFromToolbar(); await helper.waitForOverlay(); - let visibleButton = await helper.getPanelButton(".visible-page"); + let visibleButton = await helper.getPanelButton("#visible-page"); screenshotReady = TestUtils.topicObserved("screenshots-preview-ready"); visibleButton.click(); @@ -341,7 +341,7 @@ add_task(async function test_focusLastUsedMethod() { helper.triggerUIFromToolbar(); await helper.waitForOverlay(); - visibleButton = await helper.getPanelButton(".visible-page"); + visibleButton = await helper.getPanelButton("#visible-page"); screenshotReady = TestUtils.topicObserved("screenshots-preview-ready"); visibleButton.click(); @@ -370,3 +370,89 @@ add_task(async function test_focusLastUsedMethod() { await SpecialPowers.popPrefEnv(); }); + +add_task(async function testFocusedIsLocked() { + await BrowserTestUtils.withNewTab( + { + gBrowser, + url: TEST_PAGE, + }, + async browser => { + let helper = new ScreenshotsHelper(browser); + helper.triggerUIFromToolbar(); + await helper.waitForOverlay(); + + let panel = await helper.waitForPanel(); + let mozButtonGroup = panel + .querySelector("screenshots-buttons") + .shadowRoot.querySelector("moz-button-group"); + let firstButton = mozButtonGroup.firstElementChild; + let lastButton = mozButtonGroup.lastElementChild; + + firstButton.focus(); + + await BrowserTestUtils.waitForCondition(() => { + return firstButton.getRootNode().activeElement === firstButton; + }, "The first button in the panel should have focus"); + info( + `Actual focused id: ${Services.focus.focusedElement.id}. Expected focused id: ${firstButton.id}` + ); + is( + Services.focus.focusedElement, + firstButton, + "The first button in the panel should have focus" + ); + + EventUtils.synthesizeKey("KEY_Tab"); + + await BrowserTestUtils.waitForCondition(() => { + return lastButton.getRootNode().activeElement === lastButton; + }, "The last button in the panel should have focus"); + info( + `Actual focused id: ${Services.focus.focusedElement.id}. Expected focused id: ${lastButton.id}` + ); + is( + Services.focus.focusedElement, + lastButton, + "The last button in the panel should have focus" + ); + + EventUtils.synthesizeKey("KEY_Tab"); + + // Focus should move to the content document + await BrowserTestUtils.waitForCondition(() => { + return ( + firstButton.getRootNode().activeElement !== firstButton && + lastButton.getRootNode().activeElement !== lastButton + ); + }, "The first and last buttons do not have focus"); + Assert.notEqual( + Services.focus.focusedElement, + firstButton, + "The first button does not have focus" + ); + Assert.notEqual( + Services.focus.focusedElement, + lastButton, + "The last button does not have focus" + ); + + EventUtils.synthesizeKey("KEY_Tab"); + + info( + `Actual focused id: ${Services.focus.focusedElement.id}. Expected focused id: ${firstButton.id}` + ); + await BrowserTestUtils.waitForCondition(() => { + return firstButton.getRootNode().activeElement === firstButton; + }, "The first button in the panel should have focus"); + info( + `Actual focused id: ${Services.focus.focusedElement.id}. Expected focused id: ${firstButton.id}` + ); + is( + Services.focus.focusedElement, + firstButton, + "The first button in the panel should have focus" + ); + } + ); +}); diff --git a/browser/components/screenshots/tests/browser/browser_screenshots_telemetry_tests.js b/browser/components/screenshots/tests/browser/browser_screenshots_telemetry_tests.js index 0ce55eb08e15..1b9ecd0e7977 100644 --- a/browser/components/screenshots/tests/browser/browser_screenshots_telemetry_tests.js +++ b/browser/components/screenshots/tests/browser/browser_screenshots_telemetry_tests.js @@ -205,7 +205,7 @@ add_task(async function test_started_retry() { // click the visible page button in panel let visiblePageButton = panel .querySelector("screenshots-buttons") - .shadowRoot.querySelector(".visible-page"); + .shadowRoot.querySelector("#visible-page"); visiblePageButton.click(); await screenshotReady; @@ -242,7 +242,7 @@ add_task(async function test_canceled() { // click the full page button in panel let fullPageButton = panel .querySelector("screenshots-buttons") - .shadowRoot.querySelector(".full-page"); + .shadowRoot.querySelector("#full-page"); fullPageButton.click(); await screenshotReady; @@ -303,7 +303,7 @@ add_task(async function test_copy() { // click the visible page button in panel let visiblePageButton = panel .querySelector("screenshots-buttons") - .shadowRoot.querySelector(".visible-page"); + .shadowRoot.querySelector("#visible-page"); visiblePageButton.click(); info("clicked visible page, waiting for screenshots-preview-ready"); await screenshotReady; @@ -413,7 +413,7 @@ add_task(async function test_extra_telemetry() { // click the visible page button in panel let visiblePageButton = panel .querySelector("screenshots-buttons") - .shadowRoot.querySelector(".visible-page"); + .shadowRoot.querySelector("#visible-page"); visiblePageButton.click(); info("clicked visible page, waiting for screenshots-preview-ready"); await screenshotReady; @@ -429,7 +429,7 @@ add_task(async function test_extra_telemetry() { // click the full page button in panel let fullPageButton = panel .querySelector("screenshots-buttons") - .shadowRoot.querySelector(".full-page"); + .shadowRoot.querySelector("#full-page"); fullPageButton.click(); await screenshotReady; diff --git a/browser/components/screenshots/tests/browser/browser_screenshots_test_downloads.js b/browser/components/screenshots/tests/browser/browser_screenshots_test_downloads.js index 66959baa5bd0..b98499c51682 100644 --- a/browser/components/screenshots/tests/browser/browser_screenshots_test_downloads.js +++ b/browser/components/screenshots/tests/browser/browser_screenshots_test_downloads.js @@ -109,7 +109,7 @@ add_task(async function test_download_without_filepicker() { // click the visible page button in panel let visiblePageButton = panel .querySelector("screenshots-buttons") - .shadowRoot.querySelector(".visible-page"); + .shadowRoot.querySelector("#visible-page"); visiblePageButton.click(); await screenshotReady; diff --git a/browser/components/screenshots/tests/browser/browser_screenshots_test_full_page.js b/browser/components/screenshots/tests/browser/browser_screenshots_test_full_page.js index 8cbda11e32e5..92b136d3a6ed 100644 --- a/browser/components/screenshots/tests/browser/browser_screenshots_test_full_page.js +++ b/browser/components/screenshots/tests/browser/browser_screenshots_test_full_page.js @@ -41,7 +41,7 @@ add_task(async function test_fullpageScreenshot() { // click the full page button in panel let visiblePage = panel .querySelector("screenshots-buttons") - .shadowRoot.querySelector(".full-page"); + .shadowRoot.querySelector("#full-page"); visiblePage.click(); await screenshotReady; @@ -136,7 +136,7 @@ add_task(async function test_fullpageScreenshotScrolled() { // click the full page button in panel let visiblePage = panel .querySelector("screenshots-buttons") - .shadowRoot.querySelector(".full-page"); + .shadowRoot.querySelector("#full-page"); visiblePage.click(); await screenshotReady; diff --git a/browser/components/screenshots/tests/browser/browser_screenshots_test_visible.js b/browser/components/screenshots/tests/browser/browser_screenshots_test_visible.js index d2bbd4b906e7..7c207cc8cf41 100644 --- a/browser/components/screenshots/tests/browser/browser_screenshots_test_visible.js +++ b/browser/components/screenshots/tests/browser/browser_screenshots_test_visible.js @@ -45,7 +45,7 @@ add_task(async function test_visibleScreenshot() { // click the visible page button in panel let visiblePageButton = panel .querySelector("screenshots-buttons") - .shadowRoot.querySelector(".visible-page"); + .shadowRoot.querySelector("#visible-page"); visiblePageButton.click(); await screenshotReady; @@ -142,7 +142,7 @@ add_task(async function test_visibleScreenshotScrolledY() { // click the visible page button in panel let visiblePageButton = panel .querySelector("screenshots-buttons") - .shadowRoot.querySelector(".visible-page"); + .shadowRoot.querySelector("#visible-page"); visiblePageButton.click(); await screenshotReady; @@ -239,7 +239,7 @@ add_task(async function test_visibleScreenshotScrolledX() { // click the visible page button in panel let visiblePageButton = panel .querySelector("screenshots-buttons") - .shadowRoot.querySelector(".visible-page"); + .shadowRoot.querySelector("#visible-page"); visiblePageButton.click(); await screenshotReady; @@ -336,7 +336,7 @@ add_task(async function test_visibleScreenshotScrolledXAndY() { // click the visible page button in panel let visiblePageButton = panel .querySelector("screenshots-buttons") - .shadowRoot.querySelector(".visible-page"); + .shadowRoot.querySelector("#visible-page"); visiblePageButton.click(); await screenshotReady; diff --git a/browser/components/screenshots/tests/browser/head.js b/browser/components/screenshots/tests/browser/head.js index 93652b2be90b..177b986b6a5b 100644 --- a/browser/components/screenshots/tests/browser/head.js +++ b/browser/components/screenshots/tests/browser/head.js @@ -28,8 +28,8 @@ const { MAX_CAPTURE_DIMENSION, MAX_CAPTURE_AREA } = ChromeUtils.importESModule( const gScreenshotUISelectors = { panel: "#screenshotsPagePanel", - fullPageButton: "button.full-page", - visiblePageButton: "button.visible-page", + fullPageButton: "button#full-page", + visiblePageButton: "button#visible-page", copyButton: "button.#copy", };