Skip to content

Commit

Permalink
Merge pull request #174 from duhrer/GH-170
Browse files Browse the repository at this point in the history
GH-170: Fixed optional focus indicator in modals.
  • Loading branch information
duhrer authored Feb 27, 2024
2 parents 0c36179 + 037f23a commit 740ef61
Show file tree
Hide file tree
Showing 8 changed files with 91 additions and 77 deletions.
2 changes: 2 additions & 0 deletions src/css/focus-fix.css
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,15 @@
position: fixed;
top: 0;
width: 100%;
z-index: 99998;
}

.gamepad-navigator-focus-overlay-pointer {
border: 3px dashed white;
content: '';
height: 0;
mix-blend-mode: difference;
pointer-events: none;
position: absolute;
width: 0;
z-index: 99999;
Expand Down
119 changes: 76 additions & 43 deletions src/js/content_scripts/focus-overlay.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,50 +6,58 @@
fluid.defaults("gamepad.focusOverlay.pointer", {
gradeNames: ["gamepad.templateRenderer"],
markup: {
container: "<div class='gamepad-navigator-focus-overlay-pointer' hidden></div>"
container: "<div class='gamepad-navigator-focus-overlay-pointer'></div>"
},
model: {
focusOverlayElement: false,
hideFocusOverlay: true
},
modelListeners: {
focusOverlayElement: {
excludeSource: "init",
funcName: "gamepad.focusOverlay.pointer.trackFocusOverlayElement",
listeners: {
"onCreate.listenForWindowFocusEvents": {
funcName: "gamepad.focusOverlay.pointer.listenForWindowFocusEvents",
args: ["{that}"]
}
},
modelRelay: {
hideFocusOverlay: {
source: "{that}.model.hideFocusOverlay",
target: "{that}.model.dom.container.attr.hidden"
invokers: {
handleFocusin: {
funcName: "gamepad.focusOverlay.pointer.handleFocusin",
args: ["{that}", "{arguments}.0"] // event
}
},
modelListeners: {
modalManagerShadowElement: {
funcName: "gamepad.focusOverlay.pointer.listenForModalFocusEvents",
args: ["{that}"]
}
}
});

gamepad.focusOverlay.pointer.trackFocusOverlayElement = function (that) {
gamepad.focusOverlay.pointer.listenForWindowFocusEvents = function (that) {
window.addEventListener("focusin", that.handleFocusin);
};

gamepad.focusOverlay.pointer.listenForModalFocusEvents = function (that) {
var modalManagerShadowElement = fluid.get(that, "model.modalManagerShadowElement");
if (modalManagerShadowElement) {
modalManagerShadowElement.addEventListener("focusin", that.handleFocusin);
}
};

gamepad.focusOverlay.pointer.handleFocusin = function (that) {
var containerDomElement = that.container[0];
if (that.model.focusOverlayElement) {
var clientRect = that.model.focusOverlayElement.getBoundingClientRect();

// Our outline is three pixels, so we adjust everything accordingly.
containerDomElement.style.left = (clientRect.x + window.scrollX - 3) + "px";
containerDomElement.style.top = (clientRect.y + window.scrollY - 3) + "px";
containerDomElement.style.height = (clientRect.height) + "px";
containerDomElement.style.width = (clientRect.width) + "px";
var activeElement = fluid.get(that.model, "modalManagerShadowElement.activeElement") || document.activeElement;

var elementStyles = getComputedStyle(that.model.focusOverlayElement);
var borderRadiusValue = elementStyles.getPropertyValue("border-radius");
if (borderRadiusValue.length) {
containerDomElement.style.borderRadius = borderRadiusValue;
}
else {
containerDomElement.style.borderRadius = 0;
}
var clientRect = activeElement.getBoundingClientRect();

// Our outline is three pixels, so we adjust everything accordingly.
containerDomElement.style.left = (clientRect.x + window.scrollX - 3) + "px";
containerDomElement.style.top = (clientRect.y + window.scrollY - 3) + "px";
containerDomElement.style.height = (clientRect.height) + "px";
containerDomElement.style.width = (clientRect.width) + "px";

var elementStyles = getComputedStyle(activeElement);
var borderRadiusValue = elementStyles.getPropertyValue("border-radius");
if (borderRadiusValue.length) {
containerDomElement.style.borderRadius = borderRadiusValue;
}
else {
containerDomElement.style.height = 0;
containerDomElement.style.width = 0;
containerDomElement.style.borderRadius = 0;
}
};
Expand All @@ -60,22 +68,27 @@
container: "<div class='gamepad-navigator-focus-overlay'></div>"
},
model: {
activeModal: false,
shadowElement: false,

focusOverlayElement: "{gamepad.focusOverlay}.model.focusOverlayElement",
modalManagerShadowElement: false,

prefs: {},
hideFocusOverlay: true
},
modelRelay: {
hideFocusOverlay: {
source: "{that}.model.hideFocusOverlay",
target: "{that}.model.dom.container.attr.hidden"
}
},
components: {
pointer: {
container: "{that}.model.shadowElement",
type: "gamepad.focusOverlay.pointer",
createOnEvent: "onShadowReady",
options: {
model: {
focusOverlayElement: "{gamepad.focusOverlay}.model.focusOverlayElement",
hideFocusOverlay: "{gamepad.focusOverlay}.model.hideFocusOverlay"
modalManagerShadowElement: "{gamepad.focusOverlay}.model.modalManagerShadowElement"
}
}
}
Expand All @@ -86,22 +99,42 @@
funcName: "gamepad.focusOverlay.shouldDisplayOverlay",
args: ["{that}"]
},
focusOverlayElement: {
excludeSource: "init",
funcName: "gamepad.focusOverlay.shouldDisplayOverlay",
modalManagerShadowElement: {
funcName: "gamepad.focusOverlay.listenForModalFocusEvents",
args: ["{that}"]
},
activeModal: {
excludeSource: "init",
funcName: "gamepad.focusOverlay.shouldDisplayOverlay",
}
},
listeners: {
"onCreate.listenForWindowFocusEvents": {
funcName: "gamepad.focusOverlay.listenForWindowFocusEvents",
args: ["{that}"]
}
},
invokers: {
shouldDisplayOverlay: {
funcName: "gamepad.focusOverlay.shouldDisplayOverlay",
args: ["{that}", "{arguments}.0"] // event
}
}
});

gamepad.focusOverlay.shouldDisplayOverlay = function (that) {
var activeElement = fluid.get(that.model, "modalManagerShadowElement.activeElement") || document.activeElement;
var fixFocus = fluid.get(that, "model.prefs.fixFocus") ? true : false;
var hideFocusOverlay = !fixFocus || !that.model.focusOverlayElement;
var hideFocusOverlay = !fixFocus || !activeElement;
that.applier.change("hideFocusOverlay", hideFocusOverlay);
};

gamepad.focusOverlay.listenForWindowFocusEvents = function (that) {
window.addEventListener("focusin", that.shouldDisplayOverlay);
window.addEventListener("focusout", that.shouldDisplayOverlay);
};

gamepad.focusOverlay.listenForModalFocusEvents = function (that) {
var modalManagerShadowElement = fluid.get(that, "model.modalManagerShadowElement");
if (modalManagerShadowElement) {
modalManagerShadowElement.addEventListener("focusin", that.shouldDisplayOverlay);
modalManagerShadowElement.addEventListener("focusout", that.shouldDisplayOverlay);
}
};
})(fluid);
32 changes: 6 additions & 26 deletions src/js/content_scripts/input-mapper-content-utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -240,13 +240,13 @@ https://github.com/fluid-lab/gamepad-navigator/blob/main/LICENSE
// 7 elements, at position 0, add -1 would be (7 + 0 -1) % 7, or 6
that.currentTabIndex = (that.tabbableElements.length + activeElementIndex + increment) % that.tabbableElements.length;
var elementToFocus = that.tabbableElements[that.currentTabIndex];
gamepad.inputMapperUtils.content.focus(that, elementToFocus);
elementToFocus.focus();

// If focus didn't succeed, make one more attempt, to attempt to avoid focus traps (See #118).
if (!that.model.activeModal && elementToFocus !== document.activeElement) {
that.currentTabIndex = (that.tabbableElements.length + activeElementIndex + increment) % that.tabbableElements.length;
var failoverElementToFocus = that.tabbableElements[that.currentTabIndex];
gamepad.inputMapperUtils.content.focus(that, failoverElementToFocus);
failoverElementToFocus.focus();
}
}
};
Expand Down Expand Up @@ -392,7 +392,7 @@ https://github.com/fluid-lab/gamepad-navigator/blob/main/LICENSE
// Ensure that we "wrap" in both directions.
var buttonToFocusIndex = (allButtons.length + (currentButtonIndex + increment)) % allButtons.length;
var buttonToFocus = allButtons[buttonToFocusIndex];
gamepad.inputMapperUtils.content.focus(that, buttonToFocus);
buttonToFocus.focus();
buttonToFocus.click();
}
};
Expand Down Expand Up @@ -587,7 +587,7 @@ https://github.com/fluid-lab/gamepad-navigator/blob/main/LICENSE
element.classList.toggle("no-focus-indicator", false);
});

gamepad.inputMapperUtils.content.focus(that, element);
element.focus();
}
};

Expand All @@ -600,7 +600,7 @@ https://github.com/fluid-lab/gamepad-navigator/blob/main/LICENSE
};

gamepad.inputMapperUtils.content.enterFullscreen = function (that) {
if (document.fullscreen) {
if (document.fullscreenElement) {
that.vibrate();
}
else {
Expand All @@ -609,31 +609,11 @@ https://github.com/fluid-lab/gamepad-navigator/blob/main/LICENSE
};

gamepad.inputMapperUtils.content.exitFullscreen = function (that) {
if (document.fullscreen) {
if (document.fullscreenElement) {
document.exitFullscreen();
}
else {
that.vibrate();
}
};

/**
*
* Simulate focus on an element, including triggering visible focus.
*
* @param {Object} that - The input mapper component itself.
* @param {HTMLElement} element - The element to simulate focus on.
*
*/
gamepad.inputMapperUtils.content.focus = function (that, element) {
if (that.model.prefs.fixFocus) {
that.applier.change("focusOverlayElement", element);

element.addEventListener("blur", function () {
that.applier.change("focusOverlayElement", false);
});
}

element.focus();
};
})(fluid, jQuery);
5 changes: 2 additions & 3 deletions src/js/content_scripts/input-mapper.js
Original file line number Diff line number Diff line change
Expand Up @@ -225,9 +225,8 @@ https://github.com/fluid-lab/gamepad-navigator/blob/main/LICENSE
type: "gamepad.focusOverlay",
options: {
model: {
activeModal: "{gamepad.inputMapper}.model.activeModal",
focusOverlayElement: "{gamepad.inputMapper}.model.focusOverlayElement",
prefs: "{gamepad.inputMapper}.model.prefs"
prefs: "{gamepad.inputMapper}.model.prefs",
modalManagerShadowElement: "{gamepad.inputMapper}.model.shadowElement"
}
}
}
Expand Down
2 changes: 1 addition & 1 deletion src/js/content_scripts/list-selector.js
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,7 @@ https://github.com/fluid-lab/gamepad-navigator/blob/main/LICENSE

var toFocus = fluid.get(activeItems, that.model.focusedItemIndex);
if (toFocus) {
gamepad.inputMapperUtils.content.focus(that, toFocus);
toFocus.focus();
}
};

Expand Down
4 changes: 2 additions & 2 deletions src/js/content_scripts/modal.js
Original file line number Diff line number Diff line change
Expand Up @@ -166,7 +166,7 @@ https://github.com/fluid-lab/gamepad-navigator/blob/main/LICENSE
event.preventDefault();
modalManager.applier.change("activeModal", false);
if (modalManager.model.lastExternalFocused && modalManager.model.lastExternalFocused.focus) {
gamepad.inputMapperUtils.content.focus(modalManager, modalManager.model.lastExternalFocused);
modalManager.model.lastExternalFocused.focus();
}
};

Expand All @@ -188,7 +188,7 @@ https://github.com/fluid-lab/gamepad-navigator/blob/main/LICENSE
if (tabbableElements.length) {
var elementIndex = reverse ? tabbableElements.length - 1 : 0;
var elementToFocus = tabbableElements[elementIndex];
gamepad.inputMapperUtils.content.focus(that, elementToFocus);
elementToFocus.focus();
}
};

Expand Down
2 changes: 1 addition & 1 deletion src/js/content_scripts/select-operator.js
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,6 @@ https://github.com/fluid-lab/gamepad-navigator/blob/main/LICENSE
transaction.commit();

that.closeModal(event);
gamepad.inputMapperUtils.content.focus(that, selectElement);
selectElement.focus();
};
})(fluid);
2 changes: 1 addition & 1 deletion src/js/settings/bindingsPanels.js
Original file line number Diff line number Diff line change
Expand Up @@ -275,7 +275,7 @@ https://github.com/fluid-lab/gamepad-navigator/blob/main/LICENSE
var removeButtons = that.locate("removeButton");
var removeButtonToFocus = fluid.get(removeButtons, focusIndexAfterRemove);
if (removeButtonToFocus) {
gamepad.inputMapperUtils.content.focus(that, removeButtonToFocus);
removeButtonToFocus.focus();
}
}
else {
Expand Down

0 comments on commit 740ef61

Please sign in to comment.