28 |
29 |
30 |
--------------------------------------------------------------------------------
/docs/components/bindings.md:
--------------------------------------------------------------------------------
1 | # Bindings
2 |
3 | Bindings are a hash that associates an input (such as a button or thumb stick) with an [action](actions.md). Here is a
4 | simple example:
5 |
6 | ```json
7 | {
8 | "bindings": {
9 | "buttons": {
10 | "0": {
11 | "action": "click"
12 | }
13 | }
14 | }
15 | }
16 | ```
17 |
18 | This binds button 0 (on any controller) to the "click" action (see [the input mapper base grade](inputMapper.base.md)
19 | for details). A more complex example might include [action options](actions.md), as shown here:
20 |
21 | ```json
22 | {
23 | "bindings": {
24 | "axes": {
25 | "0": {
26 | "action": "scrollHorizontally",
27 | "scrollFactor": 10,
28 | "repeateRate": 0.1
29 | }
30 | }
31 | }
32 | }
33 | ```
34 |
35 | This binds the first axis (on any controller) to the `scrollHorizontally` action (see
36 | [the input mapper base grade](inputMapper.base.md) for details). The additional action options specify how far we
37 | should scroll each time the action is executed, and how often the action should repeat while the axis is still held
38 | down.
39 |
--------------------------------------------------------------------------------
/AUTHORS.md:
--------------------------------------------------------------------------------
1 |
12 |
13 | # Authors
14 |
15 | ## Copyright Holders
16 |
17 | This is the list of Gamepad Navigator copyright holders. It does not list all individual contributors because some have
18 | assigned copyright to an institution or made only minor changes. Please see the version control system's revision
19 | history for details on contributions.
20 |
21 | Copyright (c) 2023
22 |
23 | - Divyanshu Mahajan
24 | - Tony Atkins
25 | - Fluid Project
26 |
27 | ## Contributors
28 |
29 | Individual contributions can be viewed on the
30 | [Contributors](https://github.com/fluid-lab/gamepad-navigator/graphs/contributors) page, or through the version control
31 | system's revision history.
32 |
33 | _**Note**: Individual authors may not hold copyright. See above "[Copyright Holders](#copyright-holders)" section for
34 | more information._
35 |
--------------------------------------------------------------------------------
/tests/js/tab/tab-mock-utils.js:
--------------------------------------------------------------------------------
1 | /*
2 | Copyright (c) 2023 The Gamepad Navigator Authors
3 | See the AUTHORS.md file at the top-level directory of this distribution and at
4 | https://github.com/fluid-lab/gamepad-navigator/raw/main/AUTHORS.md.
5 |
6 | Licensed under the BSD 3-Clause License. You may not use this file except in
7 | compliance with this License.
8 |
9 | You may obtain a copy of the BSD 3-Clause License at
10 | https://github.com/fluid-lab/gamepad-navigator/blob/main/LICENSE
11 | */
12 |
13 | (function (fluid) {
14 | "use strict";
15 |
16 | fluid.registerNamespace("gamepad.tests.utils.buttons");
17 | fluid.registerNamespace("gamepad.tests.utils.axes");
18 |
19 | // Custom gamepad navigator component grade for tab navigation tests.
20 | fluid.defaults("gamepad.tests.tab.inputMapper", {
21 | gradeNames: ["gamepad.inputMapper.base"],
22 | windowObject: window,
23 | model: {
24 | bindings: {
25 | axes: {
26 | 2: { action: "thumbstickTabbing" }
27 | },
28 | buttons: {
29 | 4: { action: "tabBackward" }, // Left Bumper.
30 | 5: { action: "tabForward" } // Right Bumper.
31 | }
32 | }
33 | }
34 | });
35 | })(fluid);
36 |
--------------------------------------------------------------------------------
/src/js/content_scripts/templateRenderer.js:
--------------------------------------------------------------------------------
1 | /*
2 | Copyright (c) 2023 The Gamepad Navigator Authors
3 | See the AUTHORS.md file at the top-level directory of this distribution and at
4 | https://github.com/fluid-lab/gamepad-navigator/raw/main/AUTHORS.md.
5 |
6 | Licensed under the BSD 3-Clause License. You may not use this file except in
7 | compliance with this License.
8 |
9 | You may obtain a copy of the BSD 3-Clause License at
10 | https://github.com/fluid-lab/gamepad-navigator/blob/main/LICENSE
11 | */
12 | (function (fluid) {
13 | "use strict";
14 | var gamepad = gamepad || fluid.registerNamespace("gamepad");
15 |
16 | fluid.defaults("gamepad.templateRenderer", {
17 | gradeNames: ["fluid.containerRenderingView"],
18 | markup: {
19 | container: ""
20 | },
21 | model: {},
22 | invokers: {
23 | renderMarkup: {
24 | funcName: "gamepad.templateRenderer.render",
25 | args: ["{that}", "{that}.options.markup.container", "{that}.model"]
26 | }
27 | }
28 | });
29 |
30 | gamepad.templateRenderer.render = function (that, markupTemplate, model) {
31 | var renderedContent = fluid.stringTemplate(markupTemplate, model);
32 | return renderedContent;
33 | };
34 | })(fluid);
35 |
--------------------------------------------------------------------------------
/tests/js/scroll/scroll-mock-utils.js:
--------------------------------------------------------------------------------
1 | /*
2 | Copyright (c) 2023 The Gamepad Navigator Authors
3 | See the AUTHORS.md file at the top-level directory of this distribution and at
4 | https://github.com/fluid-lab/gamepad-navigator/raw/main/AUTHORS.md.
5 |
6 | Licensed under the BSD 3-Clause License. You may not use this file except in
7 | compliance with this License.
8 |
9 | You may obtain a copy of the BSD 3-Clause License at
10 | https://github.com/fluid-lab/gamepad-navigator/blob/main/LICENSE
11 | */
12 |
13 | (function (fluid) {
14 | "use strict";
15 |
16 | fluid.defaults("gamepad.tests.scroll.inputMapper", {
17 | gradeNames: ["gamepad.inputMapper.base"],
18 | windowObject: window,
19 | model: {
20 | bindings: {
21 | axes: {
22 | 0: { action: "scrollHorizontally", scrollFactor: 50},
23 | 1: { action: "scrollVertically", scrollFactor: 50}
24 | },
25 | buttons: {
26 | // D-Pad, up, down, left, right
27 | 12: { action: "scrollUp", scrollFactor: 50 },
28 | 13: { action: "scrollDown", scrollFactor: 50 },
29 | 14: { action: "scrollLeft", scrollFactor: 50 },
30 | 15: { action: "scrollRight", scrollFactor: 50 }
31 | }
32 | }
33 | }
34 | });
35 | })(fluid);
36 |
--------------------------------------------------------------------------------
/src/js/settings/draftHandlingButton.js:
--------------------------------------------------------------------------------
1 | /*
2 | Copyright (c) 2023 The Gamepad Navigator Authors
3 | See the AUTHORS.md file at the top-level directory of this distribution and at
4 | https://github.com/fluid-lab/gamepad-navigator/raw/main/AUTHORS.md.
5 |
6 | Licensed under the BSD 3-Clause License. You may not use this file except in
7 | compliance with this License.
8 |
9 | You may obtain a copy of the BSD 3-Clause License at
10 | https://github.com/fluid-lab/gamepad-navigator/blob/main/LICENSE
11 | */
12 | (function (fluid) {
13 | "use strict";
14 | fluid.defaults("gamepad.settings.draftHandlingButton", {
15 | gradeNames: ["gamepad.templateRenderer"],
16 | markup: {
17 | container: ""
18 | },
19 | model: {
20 | label: "Draft Button",
21 | disabled: true
22 | },
23 | modelRelay: {
24 | source: "{that}.model.disabled",
25 | target: "{that}.model.dom.container.attr.disabled"
26 | }
27 | });
28 |
29 | fluid.defaults("gamepad.settings.draftHandlingButton.discard", {
30 | gradeNames: ["gamepad.settings.draftHandlingButton"],
31 | model: {
32 | label: "Discard Changes"
33 | }
34 | });
35 |
36 | fluid.defaults("gamepad.settings.draftHandlingButton.save", {
37 | gradeNames: ["gamepad.settings.draftHandlingButton"],
38 | model: {
39 | label: "Save Changes"
40 | }
41 | });
42 | })(fluid);
43 |
--------------------------------------------------------------------------------
/tests/js/lib/test-utils.js:
--------------------------------------------------------------------------------
1 | /*
2 | Copyright (c) 2023 The Gamepad Navigator Authors
3 | See the AUTHORS.md file at the top-level directory of this distribution and at
4 | https://github.com/fluid-lab/gamepad-navigator/raw/main/AUTHORS.md.
5 |
6 | Licensed under the BSD 3-Clause License. You may not use this file except in
7 | compliance with this License.
8 |
9 | You may obtain a copy of the BSD 3-Clause License at
10 | https://github.com/fluid-lab/gamepad-navigator/blob/main/LICENSE
11 | */
12 |
13 | /* global gamepad */
14 |
15 | (function (fluid) {
16 | "use strict";
17 |
18 | fluid.registerNamespace("gamepad.tests.utils");
19 |
20 | // TODO: This can probably be safely removed now.
21 | /**
22 | *
23 | * Returns a dummy gamepad input model which represents that no button/axes is
24 | * disturbed.
25 | *
26 | * @return {Object} - The gamepad input model (at rest).
27 | *
28 | */
29 | gamepad.tests.utils.initializeModelAtRest = function () {
30 | var modelAtRest = {
31 | connected: true,
32 | axes: {},
33 | buttons: {}
34 | };
35 |
36 | // Initialize in accordance with the 18 buttons on the PS4 controller.
37 | for (var buttonNumber = 0; buttonNumber < 18; buttonNumber++) {
38 | modelAtRest.buttons[buttonNumber] = 0;
39 | }
40 |
41 | // Initialize in accordance with the 4 axes on the PS4 controller.
42 | for (var axesNumber = 0; axesNumber < 4; axesNumber++) {
43 | modelAtRest.axes[axesNumber] = 0;
44 | }
45 |
46 | return modelAtRest;
47 | };
48 | })(fluid);
49 |
--------------------------------------------------------------------------------
/docs/components/preferences.md:
--------------------------------------------------------------------------------
1 | # Preferences
2 |
3 | There are a few user-configurable "preferences" that are persisted to local storage and read on startup:
4 |
5 | | Key | Description | Allowed Values | Default |
6 | | --------------------- | ----------- | --------------- | ------- |
7 | | `analogCutoff` | An analog input such as a thumb stick or trigger must send a value above this number before the [input mapper](./inputMapper.base.md) will respond. | A number from `0` (all inputs allowed) to `1` (no input possible). | `0.25` |
8 | | `newTabOrWindowURL` | The URL to open when creating a new tab or window. | Any controllable URL (see below). | `https://www.google.com/` |
9 | | `openWindowOnStartup` | If no "safe" windows (see below) are open on startup, open one automatically. | `true` or `false` | `true` |
10 | | `pollingFrequency` | How often (in milliseconds) to check gamepad inputs. | A number value. | `50` |
11 | | `vibrate` | Whether to vibrate when an action cannot be completed (for example, if the user attempts to scroll down and they are already at the bottom of the page). | `true` or `false` | `true` |
12 |
13 | ## "Safe" Windows
14 |
15 | The code that allows the gamepad navigator to respond to inputs is not injected on browser internal pages such as your
16 | browser preferences. A new window that is created using a mouse or keyboard uses one of these "internal" pages as well.
17 | This is why we have two of the preferences outlined above.
18 |
19 | The `newTabOrWindowURL` preference gives us a "safe" URL to use when creating windows and tabs. The `openWindowOnStartup`
20 | setting is designed to ensure that there will always be a "safe" window available and focused on startup.
21 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | BSD 3-Clause License
2 |
3 | Copyright (c) 2020 The Gamepad Navigator Authors
4 | See the AUTHORS.md file at the top-level directory of this distribution and at
5 | https://github.com/fluid-lab/gamepad-navigator/blob/main/AUTHORS.md
6 | All rights reserved.
7 |
8 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the
9 | following conditions are met:
10 |
11 | 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following
12 | disclaimer.
13 |
14 | 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following
15 | disclaimer in the documentation and/or other materials provided with the distribution.
16 |
17 | 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote
18 | products derived from this software without specific prior written permission.
19 |
20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
21 | INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
22 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
23 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
24 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
25 | WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
26 | THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27 |
--------------------------------------------------------------------------------
/src/css/modal.css:
--------------------------------------------------------------------------------
1 | .hidden {
2 | display: none;
3 | }
4 |
5 | .modal-outer-container {
6 | align-content: center;
7 | background-color: #000c;
8 | font-family: inherit;
9 | height: 100%;
10 | left: 0;
11 | position: fixed;
12 | top: 0;
13 | width: 100%;
14 | z-index: 99998;
15 | }
16 |
17 | .modal-inner-container {
18 | background-color: white;
19 | border-radius: 1rem;
20 | color: black;
21 | display: flex;
22 | flex-direction: column;
23 | height: 75%;
24 | left: 12.5%;
25 | position: fixed;
26 | top: 12.5%;
27 | width: 75%;
28 | z-index: 99997;
29 | }
30 |
31 | @media (max-height: 400px) {
32 | :host(.gamepad-navigator-modal-manager.modal-inner-container) {
33 | height: 95%;
34 | top: 2.5%;
35 | }
36 | }
37 |
38 | @media (max-width: 600px) {
39 | :host(.gamepad-navigator-modal-manager) .modal-inner-container {
40 | left: 2.5%;
41 | width: 95%;
42 | }
43 | }
44 |
45 | .modal-header {
46 | align-items: center;
47 | align-self: center;
48 | display: flex;
49 | flex-direction: row;
50 | }
51 |
52 | .modal-header h3 {
53 | font-size: 2rem;
54 | font-weight: bold;
55 | }
56 |
57 | .modal-header .modal-icon svg {
58 | height: 5rem;
59 | width: 5rem;
60 | }
61 |
62 | .modal-body {
63 | border: 1px solid #ccc;
64 | height: 100%;
65 | overflow-y: scroll;
66 | }
67 |
68 | .modal-footer {
69 | align-self: center;
70 | }
71 |
72 | .modal-footer button {
73 | border: 1px solid #999;
74 | border-radius: 0.5rem;
75 | font-size: 2rem;
76 | margin: 1rem;
77 | padding: 0.5rem;
78 | }
79 |
80 | modal-footer button:not([disabled]) {
81 | background-color: #ccc;
82 | color: black;
83 | }
84 |
--------------------------------------------------------------------------------
/tests/html/list-selector.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | List Selector Tests
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
47 |
48 |
49 |
--------------------------------------------------------------------------------
/PRIVACY_POLICY.md:
--------------------------------------------------------------------------------
1 |
12 |
13 | # Privacy Policy
14 |
15 | The Gamepad Navigator allows the user to navigate Chromium-based browsers and web pages using a game controller. It
16 | provides various features that allow navigation using gamepad such as web page scrolling, tab navigation, click on
17 | buttons and hyperlinks, et cetera. Due to the nature of browser and web page navigation, the Gamepad Navigator requires
18 | permission to interact with the content in active web pages, browser windows, and tabs.
19 |
20 | ## Information the Gamepad Navigator collects
21 |
22 | The Gamepad Navigator makes use of user preferences entered using the extension configuration panel, which can be
23 | removed/deleted using panel buttons. To enable navigation features, the content of active websites is also parsed.
24 |
25 | ## How the Gamepad Navigator uses the information
26 |
27 | User preferences are used to determine which gamepad inputs activate browser actions. Page content is parsed to
28 | determine which focusable elements are available, so that the user can navigate between focusable elements as they would
29 | with the tab key on a keyboard.
30 |
31 | ## What information does the Gamepad Navigator share
32 |
33 | User preferences entered are stored locally on the user's computer and not shared with any site. Page content used for
34 | navigation is only retained temporarily in memory, and is not shared with any site.
35 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "gamepad-navigator",
3 | "version": "1.0.0",
4 | "description": "A Chrome extension that allows you to navigate web pages and Chromium-based browsers using a game controller.",
5 | "contributors": [
6 | { "name": "Divyanshu Mahajan" },
7 | {
8 | "name": "Tony Atkins",
9 | "url": "https://duhrer.github.io/"
10 | }
11 | ],
12 | "license": "BSD-3-Clause",
13 | "repository": {
14 | "type": "git",
15 | "url": "git+https://github.com/fluid-lab/gamepad-navigator.git"
16 | },
17 | "bugs": {
18 | "url": "https://github.com/fluid-lab/gamepad-navigator/issues"
19 | },
20 | "homepage": "https://github.com/fluid-lab/gamepad-navigator#readme",
21 | "scripts": {
22 | "lint": "fluid-lint-all",
23 | "build": "npm run build:grunt && npm run build:css && npm run build:svgs",
24 | "build:grunt": "grunt build",
25 | "build:css": "node ./utils/bundleCss.js",
26 | "build:svgs": "node ./utils/bundleSvgs.js",
27 | "postinstall": "npm run build",
28 | "test": "testem ci --file tests/testem.json"
29 | },
30 | "dependencies": {
31 | "ally.js": "1.4.1",
32 | "fluid-osk": "0.1.0-dev.20231224T071757Z.a7d1871.GH-1",
33 | "infusion": "4.7.1"
34 | },
35 | "devDependencies": {
36 | "dotenv": "16.4.5",
37 | "eslint": "9.14.0",
38 | "eslint-config-fluid": "2.1.3",
39 | "fluid-lint-all": "1.2.12",
40 | "grunt": "1.6.1",
41 | "grunt-banner": "0.6.0",
42 | "grunt-contrib-clean": "2.0.1",
43 | "grunt-contrib-compress": "2.0.0",
44 | "grunt-contrib-copy": "1.0.0",
45 | "grunt-json-prettify": "0.0.2",
46 | "grunt-prompt": "1.3.3",
47 | "node-fetch": "3.3.2",
48 | "npm-audit-resolver": "3.0.0-7",
49 | "testem": "3.15.2"
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/src/images/options-icon.svg:
--------------------------------------------------------------------------------
1 |
2 |
16 |
--------------------------------------------------------------------------------
/utils/bundleCss.js:
--------------------------------------------------------------------------------
1 | /* eslint-env node */
2 | "use strict";
3 | var fluid = require("infusion");
4 | var fs = require("fs");
5 | var path = require("path");
6 |
7 | var gamepad = fluid.registerNamespace("gamepad");
8 |
9 | fluid.defaults("gamepad.bundleCss", {
10 | gradeNames: ["fluid.component"],
11 | outputPath: "dist/js/content_scripts/styles.js",
12 | outputTemplate: "\"use strict\";\nvar gamepad = fluid.registerNamespace(\"gamepad\");\ngamepad.css = `%payload`;\n",
13 | pathsToBundle: {
14 | src: "src/css",
15 | osk: "node_modules/fluid-osk/src/css"
16 | },
17 | excludes: [
18 | "src/css/common.css",
19 | "src/css/configuration-panel.css"
20 | ],
21 | listeners: {
22 | "onCreate.processDirs": {
23 | funcName: "gamepad.bundleCss.processDirs",
24 | args: ["{that}"]
25 | }
26 | }
27 | });
28 |
29 | gamepad.bundleCss.processDirs = function (that) {
30 | var payload = "";
31 |
32 | var resolvedExcludes = that.options.excludes.map(function (relativePath) {
33 | return path.resolve(relativePath);
34 | });
35 |
36 | fluid.each(that.options.pathsToBundle, function (pathToBundle) {
37 | var filenames = fs.readdirSync(pathToBundle);
38 | fluid.each(filenames, function (filename) {
39 | if (filename.toLowerCase().endsWith(".css")) {
40 | var filePath = path.resolve(pathToBundle, filename);
41 | if (!resolvedExcludes.includes(filePath)) {
42 | var fileContents = fs.readFileSync(filePath, { encoding: "utf8"});
43 | payload += fileContents;
44 | }
45 | }
46 | });
47 | });
48 |
49 | var bundle = fluid.stringTemplate(that.options.outputTemplate, { payload: payload});
50 |
51 | fs.writeFileSync(that.options.outputPath, bundle, { encoding: "utf8"});
52 |
53 | fluid.log(fluid.logLevel.WARN, "bundled all CSS to '" + that.options.outputPath + "'");
54 | };
55 |
56 | gamepad.bundleCss();
57 |
--------------------------------------------------------------------------------
/tests/html/polling-tests.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Gamepad Navigator Polling Tests
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
\n"
81 | },
82 | selectors: {
83 | select: ".gamepad-select-input-container"
84 | },
85 | components: {
86 | select: {
87 | container: "{that}.dom.select",
88 | type: "gamepad.ui.select",
89 | options: {
90 | model: {
91 | choices: "{gamepad.ui.bindingParam.selectInput}.model.choices",
92 | noneDescription: "{gamepad.ui.bindingParam.selectInput}.model.noneDescription",
93 | noneOption: "{gamepad.ui.bindingParam.selectInput}.model.noneOption",
94 | selectedChoice: "{gamepad.ui.bindingParam.selectInput}.model.selectedChoice"
95 | }
96 | }
97 | }
98 | }
99 | });
100 | })(fluid);
101 |
--------------------------------------------------------------------------------
/src/js/content_scripts/input-mapper-background-utils.js:
--------------------------------------------------------------------------------
1 | /*
2 | Copyright (c) 2023 The Gamepad Navigator Authors
3 | See the AUTHORS.md file at the top-level directory of this distribution and at
4 | https://github.com/fluid-lab/gamepad-navigator/raw/main/AUTHORS.md.
5 |
6 | Licensed under the BSD 3-Clause License. You may not use this file except in
7 | compliance with this License.
8 |
9 | You may obtain a copy of the BSD 3-Clause License at
10 | https://github.com/fluid-lab/gamepad-navigator/blob/main/LICENSE
11 | */
12 |
13 | /* global gamepad, chrome */
14 |
15 | (function (fluid) {
16 | "use strict";
17 |
18 | fluid.registerNamespace("gamepad.inputMapperUtils.background");
19 |
20 | // TODO: Add continuous / long-press browser tab navigation, if needed.
21 | // TODO: Add browser tab navigation for thumbsticks.
22 |
23 | /**
24 | *
25 | * Connect to the background script, send a message, and handle the response.
26 | *
27 | * @param {Object} that - The inputMapper component.
28 | * @param {String} actionOptions - The action payload to be transmitted.
29 | *
30 | */
31 | gamepad.inputMapperUtils.background.postMessage = async function (that, actionOptions) {
32 | // We use this because chrome.runtime.sendMessage did not leave enough time to receive a response.
33 | var port = chrome.runtime.connect();
34 | port.onMessage.addListener(function (response) {
35 | var vibrate = fluid.get(response, "vibrate");
36 | if (vibrate) {
37 | that.vibrate();
38 | }
39 | });
40 |
41 | var wrappedActionOptions = fluid.copy(actionOptions);
42 |
43 | wrappedActionOptions.newTabOrWindowURL = that.model.prefs.newTabOrWindowURL;
44 |
45 | // Set the left pixel if the action is about changing "window size".
46 | if (actionOptions.action === "maximizeWindow" || actionOptions.action === "restoreWindowSize") {
47 | wrappedActionOptions.left = screen.availLeft;
48 | }
49 |
50 | port.postMessage(wrappedActionOptions);
51 | };
52 |
53 | /**
54 | *
55 | * Sends message to the background script to zoom in/out on the webpage using
56 | * thumbsticks.
57 | *
58 | * @param {Object} that - The inputMapper component.
59 | * @param {Object} actionOptions - The parameters for this action.
60 | * @property {Boolean} invert - Whether the zooming should be in opposite order.
61 | * @param {String} inputType - The input type ("buttons" or "axes").
62 | * @param {String|Number} index - Which button number or axis we're responding to.
63 | *
64 | */
65 | gamepad.inputMapperUtils.background.thumbstickZoom = async function (that, actionOptions, inputType, index) {
66 | var inversionFactor = fluid.get(actionOptions, "invert") ? -1 : 1;
67 |
68 | var value = fluid.get(that.model, [inputType, index]);
69 |
70 | var polarisedValue = value * inversionFactor;
71 | var zoomType = polarisedValue > 0 ? "zoomOut" : "zoomIn";
72 |
73 | var delegatedActionOptions = fluid.copy(actionOptions);
74 | delegatedActionOptions.action = zoomType;
75 | gamepad.inputMapperUtils.background.postMessage(that, delegatedActionOptions);
76 | };
77 |
78 | /**
79 | *
80 | * Sends message to the background script to change the current window size using
81 | * thumbsticks.
82 | *
83 | * @param {Object} that - The inputMapper component.
84 | * @param {Object} actionOptions - The parameters for this action.
85 | * @property {Boolean} invert - Whether the zooming should be in opposite order.
86 | * @param {String} inputType - The input type ("buttons" or "axes").
87 | * @param {String|Number} index - Which button number or axis we're responding to.
88 | *
89 | */
90 | gamepad.inputMapperUtils.background.thumbstickWindowSize = function (that, actionOptions, inputType, index) {
91 | var invert = fluid.get(actionOptions, "invert") || false;
92 |
93 | var value = fluid.get(that.model, [inputType, index]);
94 |
95 | var inversionFactor = invert ? -1 : 1;
96 | var polarisedValue = value * inversionFactor;
97 |
98 | var delegatedAction = polarisedValue > 0 ? "maximizeWindow" : "restoreWindowSize";
99 |
100 | var delegatedActionOptions = {
101 | action: delegatedAction,
102 | left: screen.availLeft
103 | };
104 | gamepad.inputMapperUtils.background.postMessage(that, delegatedActionOptions);
105 | };
106 | })(fluid);
107 |
--------------------------------------------------------------------------------
/tests/html/form-fields.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Form Field Test Page
5 |
6 |
7 |
8 |
9 |
10 |
11 |
Form Field Test Page
12 |
13 |
This page provides many examples of form elements, so that we can test them with the gamepad navigator.
14 |
15 |
Text Inputs
16 |
17 |
These inputs should be editable with the onscreen keyboard.
18 |
19 |
Input with No Type
20 |
21 |
22 |
23 |
Text Input
24 |
25 |
26 |
27 |
Search Input
28 |
29 |
30 |
31 |
Email Input
32 |
33 |
34 |
35 |
Password Input
36 |
37 |
38 |
39 |
Telephone Number Input
40 |
41 |
42 |
43 |
URL input
44 |
45 |
46 |
47 |
Text Area
48 |
49 |
50 |
51 |
Number Inputs
52 |
53 |
Number inputs should be controllable using arrow keys (which are bound to the d-pad by default)
54 |
55 |
Number Input, No Parameters
56 |
57 |
58 |
59 |
Number Input with Max / Min / Step
60 |
61 |
62 |
63 |
Range Input
64 |
65 |
66 |
67 |
"Decimal" Input Mode
68 |
69 |
70 |
71 |
Radio Buttons
72 |
73 |
No Radio Group, Nothing Selected
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
No Radio Group, Button Selected
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 |
Select Boxes
100 |
101 |
Select boxes should open with the "select operator" modal.
102 |
103 |
Select Input, Nothing Selected
104 |
105 |
110 |
111 |
Select Input, Value Selected
112 |
113 |
118 |
119 |
Select Input, Disabled Value Selected
120 |
121 |
127 |
128 |
Multi Select
129 |
130 |
135 |
136 |
Custom Select Input
137 |
138 |
139 |
140 |
155 |
156 |
157 |
158 |
--------------------------------------------------------------------------------
/tests/js/scroll/scroll-unidirectional-axes-tests.js:
--------------------------------------------------------------------------------
1 | /*
2 | Copyright (c) 2023 The Gamepad Navigator Authors
3 | See the AUTHORS.md file at the top-level directory of this distribution and at
4 | https://github.com/fluid-lab/gamepad-navigator/raw/main/AUTHORS.md.
5 |
6 | Licensed under the BSD 3-Clause License. You may not use this file except in
7 | compliance with this License.
8 |
9 | You may obtain a copy of the BSD 3-Clause License at
10 | https://github.com/fluid-lab/gamepad-navigator/blob/main/LICENSE
11 | */
12 |
13 | /* global gamepad, jqUnit */
14 |
15 | (function (fluid, $) {
16 | "use strict";
17 |
18 | $(document).ready(function () {
19 |
20 | fluid.registerNamespace("gamepad");
21 | fluid.registerNamespace("gamepad.tests");
22 |
23 | gamepad.tests.delayMs = 250;
24 |
25 | jqUnit.module("Gamepad Navigator Unidirectional Axes Scrolling Tests");
26 |
27 | jqUnit.test("Scroll down using axes input", function () {
28 | jqUnit.expect(2);
29 |
30 | // Initialize the webpage, i.e., scroll the page to the top.
31 | $(window).scrollTop(0);
32 |
33 | var inputMapper = gamepad.tests.scroll.inputMapper();
34 |
35 | jqUnit.assertEquals("The initial vertical scroll position should not be changed.", 0, window.scrollY);
36 |
37 | // Update the gamepad to tilt axes 1 for for scrolling.
38 | inputMapper.applier.change("axes.1", 1);
39 |
40 | jqUnit.stop();
41 |
42 | // Wait for a few milliseconds for the webpage to scroll.
43 | setTimeout(function () {
44 | jqUnit.start();
45 |
46 | // Check if the gamepad has scrolled down.
47 | jqUnit.assertNotEquals("The page should have scrolled down.", 0, window.scrollY);
48 | }, gamepad.tests.delayMs);
49 | });
50 |
51 | jqUnit.test("Scroll up using axes input", function () {
52 | jqUnit.expect(2);
53 |
54 | // Initialize the webpage, i.e., scroll the page towards the bottom.
55 | $(window).scrollTop(400);
56 |
57 | var inputMapper = gamepad.tests.scroll.inputMapper();
58 |
59 | jqUnit.assertEquals("The initial vertical scroll position should not be changed.", 400, window.scrollY);
60 |
61 | // Update the gamepad to tilt axes 1 for for scrolling.
62 | inputMapper.applier.change("axes.1", -1);
63 |
64 | jqUnit.stop();
65 |
66 | // Wait for a few milliseconds for the webpage to scroll.
67 | setTimeout(function () {
68 | jqUnit.start();
69 |
70 | // Check if the gamepad has scrolled up.
71 | var hasScrolledUp = window.scrollY < 400;
72 | jqUnit.assertTrue("The page should have scrolled up.", hasScrolledUp);
73 | }, gamepad.tests.delayMs);
74 | });
75 |
76 | jqUnit.test("Scroll right using axes input", function () {
77 | jqUnit.expect(2);
78 |
79 | // Initialize the webpage, i.e., scroll the page to the left.
80 | $(window).scrollLeft(0);
81 |
82 | var inputMapper = gamepad.tests.scroll.inputMapper();
83 |
84 | jqUnit.assertEquals("The horizontal vertical scroll position should not be changed.", 0, window.scrollX);
85 |
86 | // Update the gamepad to tilt axes 0 for for scrolling.
87 | inputMapper.applier.change("axes.0", 1);
88 |
89 | jqUnit.stop();
90 |
91 | // Wait for a few milliseconds for the webpage to scroll.
92 | setTimeout(function () {
93 | jqUnit.start();
94 |
95 | // Check if the gamepad has scrolled towards the right.
96 | jqUnit.assertNotEquals("The page should have scrolled right.", 0, window.scrollX);
97 | }, gamepad.tests.delayMs);
98 | });
99 |
100 | jqUnit.test("Scroll left using axes input", function () {
101 | jqUnit.expect(2);
102 |
103 | // Initialize the webpage, i.e., scroll the page towards the right.
104 | $(window).scrollLeft(400);
105 |
106 | var inputMapper = gamepad.tests.scroll.inputMapper();
107 |
108 | jqUnit.assertEquals("The horizontal vertical scroll position should not be changed.", 400, window.scrollX);
109 |
110 | // Update the gamepad to tilt axes 0 for for scrolling.
111 | inputMapper.applier.change("axes.0", -1);
112 |
113 | jqUnit.stop();
114 |
115 | // Wait for a few milliseconds for the webpage to scroll.
116 | setTimeout(function () {
117 | jqUnit.start();
118 |
119 | // Check if the gamepad has scrolled towards the left.
120 | var hasScrolledLeft = window.scrollX < 400;
121 | jqUnit.assertTrue("The page should have scrolled left.", hasScrolledLeft);
122 | }, gamepad.tests.delayMs);
123 | });
124 | });
125 | })(fluid, jQuery);
126 |
--------------------------------------------------------------------------------
/docs/components/navigator.md:
--------------------------------------------------------------------------------
1 |
12 |
13 | # `gamepad.navigator`
14 |
15 | This component listens to the `gamepadconnected` and `gamepaddisconnected` events in the browser. When at least one
16 | gamepad is connected, it reads inputs from them at a particular frequency and stores those input values in the
17 | component's model data. The component stops reading these inputs when the last gamepad is disconnected.
18 |
19 | ## Using this grade
20 |
21 | You can change the configuration options by either extending the component or by passing your own options. If no
22 | options are passed, the defaults are used.
23 |
24 | ``` javascript
25 | // Either create an instance of the navigator.
26 | var navigatorInstanceOne = gamepad.navigator();
27 |
28 | // Otherwise create a custom component using the navigator grade.
29 | fluid.defaults("my.navigator.grade", {
30 | gradeNames: ["gamepad.navigator"]
31 | });
32 | var navigatorInstanceTwo = my.navigator.grade();
33 | ```
34 |
35 | ## Component Options
36 |
37 | The following component configuration options are supported:
38 |
39 | | Option | Type | Default Value | Description |
40 | | :---: | :---: | :---: | :--- |
41 | | `windowObject` | `{Object}` | `window` | The global window object from which the navigator derives various methods to read gamepad inputs and attach listeners to it for various gamepad events. |
42 | | `frequency` | `{Integer}` | 50 | The frequency (in ms) at which the navigator will read gamepad inputs. |
43 |
44 | These options can be provided to the component in the following ways:
45 |
46 | ```javascript
47 | // Either pass the options inside the object as an argument while creating an instance of the navigator.
48 | var navigatorInstanceOne = gamepad.navigator({
49 | windowObject: myCustomWindowObject,
50 | frequency: 100
51 | });
52 |
53 | // Otherwise pass it as default options in a custom component using the navigator grade.
54 | fluid.defaults("my.navigator.grade", {
55 | gradeNames: ["gamepad.navigator"],
56 | windowObject: myCustomWindowObject,
57 | frequency: 100
58 | });
59 | var navigatorInstanceTwo = my.navigator.grade();
60 | ```
61 |
62 | Though the `windowObject` is a configurable option, it should have all the properties and methods that the navigator
63 | uses. Otherwise, the navigator won't work and will throw errors. You can also use a modified `window` object, such as
64 | the gamepad mocks used in this package's tests, and pass it into the component if you want to achieve a different
65 | behavior. That'll be less likely to throw errors.
66 |
67 | ## Invokers
68 |
69 | ### `{navigator}.attachListener()`
70 |
71 | - Returns: Nothing.
72 |
73 | Attaches event listeners to the `windowObject` for the `gamepadconnected` and `gamepaddisconnected` events. When these
74 | events are fired, the navigator fires its native `onGamepadConnected` or `onGamepadDisconnected` events, depending upon
75 | the former event. These events have listeners attached to it, which start reading the gamepad inputs or stop reading it
76 | further.
77 |
78 | ### `{navigator}.onConnected()`
79 |
80 | - Returns: Nothing.
81 |
82 | Listens for the gamepad navigator component's event `onGamepadConnected`. When it is called, it initiates the gamepad
83 | polling invoker [`{navigator}.pollGamepads`](#navigatorpollgamepads) to be called at the same frequency as specified in
84 | the component's configurable option `frequency`. The interval ID is stored in the component's member
85 | `connectivityIntervalReference` for reference in other invokers and listeners present in the component.
86 |
87 | ### `{navigator}.pollGamepads()`
88 |
89 | - Returns: Nothing.
90 |
91 | Called periodically to read the gamepad inputs and to update the navigator component's model with those values. The
92 | inputs are read from all the connected gamepads and then combine them to provide the co-pilot mode experience.
93 |
94 | ### `{navigator}.onDisconnected()`
95 |
96 | - Returns: Nothing.
97 |
98 | Listens for the gamepad navigator component's event `onGamepadDisconnected`. When it is called, it stops the polling
99 | function interval loop and then checks whether any gamepad is still connected. If any gamepad is found connected, it
100 | will fire the `onGamepadConnected` event so that the navigator continues to read gamepad inputs. Otherwise, the
101 | navigator will restore the component's model to its initial state (when no gamepad was connected).
102 |
103 | ### `{navigator}.clearConnectivityInterval()`
104 |
105 | - Returns: Nothing.
106 |
107 | Listens for the navigator component's `onDestroy` event and will clear the polling function interval loop when called.
108 |
--------------------------------------------------------------------------------
/tests/html/advanced-focus.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | Advanced Focus Tester
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
Advanced Focus Tester
13 |
14 |
15 | This page demonstrates a range of advanced elements to confirm whether
16 | we can navigate to and control them.
17 |
18 |
19 |
Simple Elements
20 |
21 |
22 | Here are a few simple elements that we can easily work with. We use
23 | these to ensure that our approach to advanced elements does not break
24 | handling of simple elements.
25 |
44 | Specialised controls like <video> elements also make use of a
45 | shadow root, but do not expose their internals. We have a preference
46 | (enabled by default) to expose controls automatically. Clicking a
47 | media element with its controls exposed should play or pause.
48 |
49 |
50 |
Video, Controls Enabled by Default
51 |
52 |
55 |
56 |
Video, Controls Disabled by Default
57 |
58 |
61 |
62 |
Shadow Content
63 |
64 |
65 | Some elements make use of a "shadow root" to store their associated DOM
66 | elements. These cannot be found using many query strategies, and it is
67 | important that we find them and any "tabbable" sub-elements. This page
68 | provides a mix of normal and shadow elements to test tab focus.
69 |
70 |
71 |
Shadow Content, Open Container
72 |
73 |
74 |
75 |
Shadow Content, Closed Container
76 |
77 |
78 |
79 |
IFrame Content
80 |
81 |
There are also <iframe> elements, which present their own challenges.
82 |
83 |
84 |
85 |
132 |
133 |
--------------------------------------------------------------------------------
/tests/js/scroll/scroll-unidirectional-button-tests.js:
--------------------------------------------------------------------------------
1 | /*
2 | Copyright (c) 2023 The Gamepad Navigator Authors
3 | See the AUTHORS.md file at the top-level directory of this distribution and at
4 | https://github.com/fluid-lab/gamepad-navigator/raw/main/AUTHORS.md.
5 |
6 | Licensed under the BSD 3-Clause License. You may not use this file except in
7 | compliance with this License.
8 |
9 | You may obtain a copy of the BSD 3-Clause License at
10 | https://github.com/fluid-lab/gamepad-navigator/blob/main/LICENSE
11 | */
12 |
13 | /* global gamepad, jqUnit */
14 |
15 | (function (fluid, $) {
16 | "use strict";
17 |
18 | $(document).ready(function () {
19 |
20 | fluid.registerNamespace("gamepad");
21 | fluid.registerNamespace("gamepad.tests");
22 |
23 | gamepad.tests.windowObject = window;
24 | gamepad.tests.delayMs = 250;
25 |
26 | jqUnit.module("Gamepad Navigator Unidirectional Button Scrolling Tests");
27 |
28 | jqUnit.test("Scroll down using button input", function () {
29 | jqUnit.expect(2);
30 |
31 | // Initialize the webpage, i.e., scroll the page to the top.
32 | $(window).scrollTop(0);
33 |
34 | var inputMapper = gamepad.tests.scroll.inputMapper();
35 |
36 | jqUnit.assertEquals("The initial vertical scroll position should not be changed.", 0, window.scrollY);
37 |
38 | // Update the gamepad to press button 13 for scrolling.
39 | inputMapper.applier.change("buttons.13", 1);
40 |
41 | jqUnit.stop();
42 |
43 | // Wait for a few milliseconds for the webpage to scroll.
44 | setTimeout(function () {
45 | jqUnit.start();
46 |
47 | // Check if the gamepad has scrolled down.
48 | jqUnit.assertNotEquals("The page should have scrolled down.", 0, window.scrollY);
49 | }, gamepad.tests.delayMs);
50 | });
51 |
52 | jqUnit.test("Scroll up using button input", function () {
53 | jqUnit.expect(2);
54 |
55 | // Initialize the webpage, i.e., scroll the page towards the bottom.
56 | $(window).scrollTop(400);
57 |
58 | jqUnit.assertEquals("The initial vertical scroll position should not be changed.", 400, window.scrollY);
59 |
60 | var inputMapper = gamepad.tests.scroll.inputMapper();
61 |
62 | // Update the gamepad to press button 12 for scrolling.
63 | inputMapper.applier.change("buttons.12", 1);
64 |
65 | jqUnit.stop();
66 |
67 | // Wait for a few milliseconds for the webpage to scroll.
68 | setTimeout(function () {
69 | jqUnit.start();
70 |
71 | // Check if the gamepad has scrolled up.
72 | var hasScrolledUp = window.scrollY < 400;
73 | jqUnit.assertTrue("The page should have scrolled up.", hasScrolledUp);
74 | }, gamepad.tests.delayMs);
75 | });
76 |
77 | jqUnit.test("Scroll right using button input", function () {
78 | jqUnit.expect(2);
79 |
80 | // Initialize the webpage, i.e., scroll the page to the left.
81 | $(window).scrollLeft(0);
82 |
83 | var inputMapper = gamepad.tests.scroll.inputMapper();
84 |
85 | jqUnit.assertEquals("The horizontal vertical scroll position should not be changed.", 0, window.scrollX);
86 |
87 | // Update the gamepad to press button 15 for scrolling.
88 | inputMapper.applier.change("buttons.15", 1);
89 |
90 | jqUnit.stop();
91 |
92 | // Wait for a few milliseconds for the webpage to scroll.
93 | setTimeout(function () {
94 | jqUnit.start();
95 |
96 | // Check if the gamepad has scrolled towards the right.
97 | jqUnit.assertNotEquals("The page should have scrolled right.", 0, window.scrollX);
98 | }, gamepad.tests.delayMs);
99 | });
100 |
101 | jqUnit.test("Scroll left using button input", function () {
102 | jqUnit.expect(2);
103 |
104 | // Initialize the webpage, i.e., scroll the page towards the right.
105 | $(window).scrollLeft(400);
106 |
107 | var inputMapper = gamepad.tests.scroll.inputMapper();
108 |
109 | jqUnit.assertEquals("The horizontal vertical scroll position should not be changed.", 400, window.scrollX);
110 |
111 | // Update the gamepad to press button 14 for scrolling.
112 | inputMapper.applier.change("buttons.14", 1);
113 |
114 | jqUnit.stop();
115 |
116 | // Wait for a few milliseconds for the webpage to scroll.
117 | setTimeout(function () {
118 | jqUnit.start();
119 |
120 | // Check if the gamepad has scrolled towards the left.
121 | var hasScrolledLeft = window.scrollX < 400;
122 | jqUnit.assertTrue("The page should have scrolled left.", hasScrolledLeft);
123 | }, gamepad.tests.delayMs);
124 | });
125 | });
126 | })(fluid, jQuery);
127 |
--------------------------------------------------------------------------------
/tests/js/polling/gamepad-polling-tests.js:
--------------------------------------------------------------------------------
1 | /*
2 | Copyright (c) 2023 The Gamepad Navigator Authors
3 | See the AUTHORS.md file at the top-level directory of this distribution and at
4 | https://github.com/fluid-lab/gamepad-navigator/raw/main/AUTHORS.md.
5 |
6 | Licensed under the BSD 3-Clause License. You may not use this file except in
7 | compliance with this License.
8 |
9 | You may obtain a copy of the BSD 3-Clause License at
10 | https://github.com/fluid-lab/gamepad-navigator/blob/main/LICENSE
11 | */
12 | /* global jqUnit */
13 | (function (fluid) {
14 | "use strict";
15 | var gamepad = fluid.registerNamespace("gamepad");
16 |
17 | fluid.defaults("gamepad.test.repeatRate.navigator", {
18 | gradeNames: ["gamepad.navigator"]
19 | });
20 |
21 | var navigator;
22 | var windowMock;
23 | jqUnit.module("Gamepad Navigator Gamepad Polling Tests", {
24 | setup: function () {
25 | windowMock = gamepad.test.window();
26 | navigator = gamepad.test.repeatRate.navigator({
27 | windowObject: windowMock
28 | });
29 | }
30 | });
31 |
32 | jqUnit.test("Polling should not occur when no gamepads are connected.", function () {
33 | var initialValue = fluid.get(navigator, "model.buttons.0") || 0;
34 | jqUnit.assertEquals("No button value should have been recorded on startup.", 0, initialValue);
35 |
36 | // Create the enclosing gamepad definition, but flag it as disconnected.
37 | windowMock.applier.change("gamepads.0", { disconnected: true });
38 | // Now simulate a button press.
39 | windowMock.applier.change("gamepads.0.buttons.0", 1);
40 |
41 | jqUnit.stop();
42 | setTimeout(function () {
43 | jqUnit.start();
44 |
45 | var updatedValue = fluid.get(navigator, "model.buttons.0") || 0;
46 |
47 | jqUnit.assertEquals("There should still be no button value after several polling cycles have elapsed.", 0, updatedValue);
48 | }, (navigator.model.prefs.pollingFrequency * 2));
49 | });
50 |
51 | jqUnit.test("Polling should start when a gamepad is connected.", function () {
52 | var initialValue = fluid.get(navigator, "model.buttons.0") || 0;
53 | jqUnit.assertEquals("No button value should have been recorded on startup.", 0, initialValue);
54 |
55 | // Simulate a button press.
56 | windowMock.applier.change("gamepads.0.buttons.0", 1);
57 | windowMock.events.gamepadconnected.fire();
58 |
59 | jqUnit.stop();
60 | setTimeout(function () {
61 | jqUnit.start();
62 |
63 | var updatedValue = fluid.get(navigator, "model.buttons.0") || 0;
64 |
65 | jqUnit.assertEquals("There should be a button value after several polling cycles have elapsed.", 1, updatedValue);
66 | }, (navigator.model.prefs.pollingFrequency * 2));
67 | });
68 |
69 | jqUnit.test("Polling should continue if only some gamepads are disconnected.", function () {
70 | var initialValue = fluid.get(navigator, "model.buttons.0") || 0;
71 | jqUnit.assertEquals("No button value should have been recorded on startup.", 0, initialValue);
72 |
73 | // Create a connected gamepad and simulate a button press.
74 | windowMock.applier.change("gamepads.0.buttons.0", 1);
75 | windowMock.events.gamepadconnected.fire();
76 |
77 | windowMock.applier.change("gamepads.1.buttons.1", 1);
78 | windowMock.applier.change("gamepads.1.disconnected", true);
79 |
80 | windowMock.events.gamepaddisconnected.fire();
81 |
82 | jqUnit.stop();
83 | setTimeout(function () {
84 | jqUnit.start();
85 |
86 | var updatedValue = fluid.get(navigator, "model.buttons.0") || 0;
87 |
88 | jqUnit.assertEquals("There should be a button value after several polling cycles have elapsed.", 1, updatedValue);
89 | }, (navigator.model.prefs.pollingFrequency * 2));
90 | });
91 |
92 |
93 | jqUnit.test("Polling should stop when the last gamepad is disconnected.", function () {
94 | var timeToWait = (navigator.model.prefs.pollingFrequency * 2);
95 | var initialValue = fluid.get(navigator, "model.buttons.0") || 0;
96 | jqUnit.assertEquals("No button value should have been recorded on startup.", 0, initialValue);
97 |
98 | // Create a connected gamepad and simulate a button press.
99 | windowMock.applier.change("gamepads.0.buttons.0", 1);
100 | windowMock.events.gamepadconnected.fire();
101 |
102 | jqUnit.stop();
103 | setTimeout(function () {
104 | jqUnit.start();
105 |
106 | var updatedValue = fluid.get(navigator, "model.buttons.0") || 0;
107 |
108 | jqUnit.assertEquals("There should be a button value after several polling cycles have elapsed.", 1, updatedValue);
109 |
110 | windowMock.applier.change("gamepads.0.disconnected", true);
111 | windowMock.events.gamepaddisconnected.fire();
112 |
113 | jqUnit.stop();
114 |
115 | setTimeout(function () {
116 | jqUnit.start();
117 | var valueAfterDisconnect = fluid.get(navigator, "model.buttons.0") || 0;
118 |
119 | jqUnit.assertEquals("The button value should have been cleared after controller disconnect.", 0, valueAfterDisconnect);
120 | }, timeToWait);
121 | }, timeToWait);
122 | });
123 | })(fluid);
124 |
--------------------------------------------------------------------------------
/tests/js/tab/tab-button-discrete-tests.js:
--------------------------------------------------------------------------------
1 | /*
2 | Copyright (c) 2023 The Gamepad Navigator Authors
3 | See the AUTHORS.md file at the top-level directory of this distribution and at
4 | https://github.com/fluid-lab/gamepad-navigator/raw/main/AUTHORS.md.
5 |
6 | Licensed under the BSD 3-Clause License. You may not use this file except in
7 | compliance with this License.
8 |
9 | You may obtain a copy of the BSD 3-Clause License at
10 | https://github.com/fluid-lab/gamepad-navigator/blob/main/LICENSE
11 | */
12 |
13 | /* global gamepad, jqUnit */
14 |
15 | (function (fluid, $) {
16 | "use strict";
17 |
18 | $(document).ready(function () {
19 |
20 | fluid.registerNamespace("gamepad.tests");
21 |
22 | gamepad.tests.delayMs = 250;
23 |
24 | jqUnit.module("Gamepad Navigator Button (Discrete) Tab Navigation Tests");
25 |
26 | jqUnit.test("Tab from the last element to the first in discrete forward tabbing using buttons.", function () {
27 | jqUnit.expect(1);
28 |
29 | // Set initial conditions i.e., focus on the last element.
30 | $("#last").focus();
31 |
32 | var inputMapper = gamepad.tests.tab.inputMapper();
33 |
34 | /**
35 | * Update the gamepad to press button 5 (right bumper) for forward tab
36 | * navigation.
37 | */
38 | inputMapper.applier.change("buttons.5", 1);
39 |
40 | jqUnit.stop();
41 |
42 | /**
43 | * Wait for a few milliseconds for the navigator to focus.
44 | *
45 | * This is a race condition as the tab navigation is asynchronous and uses
46 | * setInterval for continuous tabbing when button is pressed but not released.
47 | */
48 | setTimeout(function () {
49 | jqUnit.start();
50 |
51 | // Check if the first element is focused.
52 | jqUnit.assertEquals("The first element (with tabindex=1) should be focused.", document.querySelector("#first"), document.activeElement);
53 | }, gamepad.tests.delayMs);
54 | });
55 |
56 | jqUnit.test("Tab from the first element to the last in discrete reverse tabbing using buttons.", function () {
57 | jqUnit.expect(1);
58 |
59 | // Set initial conditions i.e., focus on the first element.
60 | $("#first").focus();
61 |
62 | var inputMapper = gamepad.tests.tab.inputMapper();
63 |
64 | /**
65 | * Update the gamepad to press button 4 (left bumper) for reverse tab
66 | * navigation.
67 | */
68 | inputMapper.applier.change("buttons.4", 1);
69 |
70 | jqUnit.stop();
71 |
72 | /**
73 | * Wait for a few milliseconds for the navigator to focus.
74 | *
75 | * This is a race condition as the tab navigation is asynchronous and uses
76 | * setInterval for continuous tabbing when button is pressed but not released.
77 | */
78 | setTimeout(function () {
79 | jqUnit.start();
80 |
81 | // Check if the last element is focused.
82 | jqUnit.assertEquals("The last element should be focused.", document.querySelector("#last"), document.activeElement);
83 | }, gamepad.tests.delayMs);
84 | });
85 |
86 | jqUnit.test("Change the focus to the next element in discrete forward tabbing using buttons.", function () {
87 | jqUnit.expect(1);
88 |
89 | // Set initial conditions i.e., focus on the first element.
90 | $("#first").focus();
91 |
92 | var inputMapper = gamepad.tests.tab.inputMapper();
93 |
94 | /**
95 | * Update the gamepad to press button 5 (right bumper) for forward tab
96 | * navigation.
97 | */
98 | inputMapper.applier.change("buttons.5", 1);
99 |
100 | jqUnit.stop();
101 |
102 | // Wait for a few milliseconds for the navigator to change focus.
103 | setTimeout(function () {
104 | jqUnit.start();
105 |
106 | // Check if the focus has moved to the next element.
107 | jqUnit.assertEquals("The focus should have moved to the second element.", document.querySelector("#second"), document.activeElement);
108 | }, gamepad.tests.delayMs);
109 | });
110 |
111 | jqUnit.test("Change the focus to the previous element in discrete reverse tabbing using buttons.", function () {
112 | jqUnit.expect(1);
113 |
114 | // Set initial conditions i.e., focus on some element in the middle.
115 | $("#fifth").focus();
116 |
117 | var inputMapper = gamepad.tests.tab.inputMapper();
118 |
119 | /**
120 | * Update the gamepad to press button 4 (right bumper) for reverse tab
121 | * navigation.
122 | */
123 | inputMapper.applier.change("buttons.4", 1);
124 |
125 | jqUnit.stop();
126 |
127 | // Wait for a few milliseconds for the navigator to change focus.
128 | setTimeout(function () {
129 | jqUnit.start();
130 |
131 | // Check if the focus has moved to the previous element.
132 | jqUnit.assertEquals("The focus should have moved to the fourth element.", document.querySelector("#fourth"), document.activeElement);
133 | }, gamepad.tests.delayMs);
134 | });
135 | });
136 | })(fluid, jQuery);
137 |
--------------------------------------------------------------------------------
/src/js/settings/addBinding.js:
--------------------------------------------------------------------------------
1 | /*
2 | Copyright (c) 2023 The Gamepad Navigator Authors
3 | See the AUTHORS.md file at the top-level directory of this distribution and at
4 | https://github.com/fluid-lab/gamepad-navigator/raw/main/AUTHORS.md.
5 |
6 | Licensed under the BSD 3-Clause License. You may not use this file except in
7 | compliance with this License.
8 |
9 | You may obtain a copy of the BSD 3-Clause License at
10 | https://github.com/fluid-lab/gamepad-navigator/blob/main/LICENSE
11 | */
12 | (function (fluid) {
13 | "use strict";
14 | var gamepad = fluid.registerNamespace("gamepad");
15 |
16 | fluid.defaults("gamepad.settings.ui.addBinding.selectWithNone", {
17 | gradeNames: ["gamepad.ui.select"],
18 | model: {
19 | noneOption: true,
20 | noneDescription: "Select an option"
21 | }
22 | });
23 |
24 | fluid.defaults("gamepad.settings.ui.addBinding", {
25 | gradeNames: ["gamepad.templateRenderer"],
26 | injectionType: "replaceWith",
27 | markup: {
28 | container: "
\nWhen the \nis pressed,\n\n\n
"
29 | },
30 | model: {
31 | hidden: false,
32 |
33 | action: "none",
34 | actionChoices: {},
35 |
36 | index: "none",
37 | availableIndexChoices: {},
38 |
39 | missingParams: true
40 | },
41 | modelListeners: {
42 | hidden: {
43 | this: "{that}.container",
44 | method: "toggleClass",
45 | args: ["hidden", "{change}.value"]
46 | },
47 | availableIndexChoices: {
48 | funcName: "gamepad.settings.ui.addBinding.checkIndexChoices",
49 | args: ["{that}"]
50 | },
51 | action: {
52 | funcName: "gamepad.settings.ui.addBinding.checkForMissingParams",
53 | args: ["{that}"]
54 | },
55 | index: {
56 | funcName: "gamepad.settings.ui.addBinding.checkForMissingParams",
57 | args: ["{that}"]
58 | }
59 | },
60 | modelRelay: {
61 | source: "{that}.model.missingParams",
62 | target: "{that}.model.dom.addButton.attr.disabled"
63 | },
64 | selectors: {
65 | actionSelect: ".gamepad-settings-add-binding-action-select",
66 | indexSelect: ".gamepad-settings-add-binding-index-select",
67 | addButton: ".gamepad-settings-add-binding-addButton"
68 | },
69 | // TODO: These throw errors about length, whether here or in the subcomponent definition in the parent. Fix.
70 | invokers: {
71 | handleClick: {
72 | funcName: "gamepad.settings.ui.addBinding.notifyParent",
73 | args: ["{that}", "{arguments}.0", "{gamepad.settings.ui.bindingsPanel}"] // event, parentComponent
74 | }
75 | },
76 | listeners: {
77 | "onCreate.bindAddButton": {
78 | this: "{that}.dom.addButton",
79 | method: "click",
80 | args: ["{that}.handleClick"]
81 | }
82 | },
83 | components: {
84 | indexSelect: {
85 | container: "{that}.dom.indexSelect",
86 | type: "gamepad.settings.ui.addBinding.selectWithNone",
87 | options: {
88 | model: {
89 | noneDescription: "- select an input -",
90 | selectedChoice: "{gamepad.settings.ui.addBinding}.model.index",
91 | // Unlike the bindings, we can use availableIndexChoices directly.
92 | choices: "{gamepad.settings.ui.bindingsPanel}.model.availableIndexChoices"
93 | }
94 | }
95 | },
96 | actionSelect: {
97 | container: "{that}.dom.actionSelect",
98 | type: "gamepad.settings.ui.addBinding.selectWithNone",
99 | options: {
100 | model: {
101 | noneDescription: "- select an action -",
102 | selectedChoice: "{gamepad.settings.ui.addBinding}.model.action",
103 | choices: "{gamepad.settings.ui.addBinding}.model.actionChoices"
104 | }
105 | }
106 | }
107 | }
108 | });
109 |
110 | gamepad.settings.ui.addBinding.checkForMissingParams = function (that) {
111 | var missingParams = !that.model.index || that.model.index === "none" || !that.model.action || that.model.action === "none";
112 | that.applier.change("missingParams", missingParams);
113 | };
114 |
115 | gamepad.settings.ui.addBinding.notifyParent = function (that, event, parentComponent) {
116 | event.preventDefault();
117 |
118 | parentComponent.addBinding(that.model);
119 |
120 | that.applier.change("action", false);
121 | that.applier.change("index", false);
122 | };
123 |
124 | gamepad.settings.ui.addBinding.checkIndexChoices = function (that) {
125 | var hasAvailableIndexChoices = typeof that.model.availableIndexChoices === "object" && Object.keys(that.model.availableIndexChoices).length > 0;
126 | that.applier.change("hidden", !hasAvailableIndexChoices);
127 | };
128 | })(fluid);
129 |
--------------------------------------------------------------------------------
/tests/js/polling/action-repeat-tests.js:
--------------------------------------------------------------------------------
1 | /*
2 | Copyright (c) 2023 The Gamepad Navigator Authors
3 | See the AUTHORS.md file at the top-level directory of this distribution and at
4 | https://github.com/fluid-lab/gamepad-navigator/raw/main/AUTHORS.md.
5 |
6 | Licensed under the BSD 3-Clause License. You may not use this file except in
7 | compliance with this License.
8 |
9 | You may obtain a copy of the BSD 3-Clause License at
10 | https://github.com/fluid-lab/gamepad-navigator/blob/main/LICENSE
11 | */
12 | /* global jqUnit */
13 | (function (fluid) {
14 | "use strict";
15 | var gamepad = fluid.registerNamespace("gamepad");
16 |
17 | jqUnit.module("Gamepad Navigator Repeat Rate Tests");
18 |
19 | // Test instance of mapper with a non-repeating action bound to
20 | // button/axes 0, and a repeating action bound to button/axes 1.
21 | fluid.defaults("gamepad.test.repeatRate.inputMapper", {
22 | gradeNames: ["gamepad.inputMapper.base"],
23 | windowObject: window,
24 | model: {
25 | count: 0,
26 | bindings: {
27 | axes: { 0: { action: "count" }, 1: { action: "count", repeatRate: 0.3 }},
28 | buttons: { 0: { action: "count" }, 1: { action: "count", repeatRate: 0.3 }}
29 | }
30 | },
31 | invokers: {
32 | "count": {
33 | funcName: "gamepad.test.repeatRate.inputMapper.updateCount",
34 | args: ["{that}"]
35 | }
36 | }
37 | });
38 |
39 | gamepad.test.repeatRate.inputMapper.updateCount = function (that) {
40 | var currentCount = that.model.count || 0;
41 | that.applier.change("count", currentCount + 1);
42 | };
43 |
44 | fluid.each(["axes", "buttons"], function (inputType) {
45 | jqUnit.test("Non-repeating actions bound to " + inputType + " should not repeat.", function () {
46 | var inputMapper = gamepad.test.repeatRate.inputMapper();
47 |
48 | jqUnit.assertEquals("No actions should have been executed on startup.", 0, inputMapper.model.count);
49 |
50 | inputMapper.applier.change([inputType, 0], 1);
51 |
52 | jqUnit.assertEquals("The initial " + inputType + " press should have fired an action.", 1, inputMapper.model.count);
53 |
54 | jqUnit.stop();
55 |
56 | setTimeout(function () {
57 | jqUnit.start();
58 |
59 | jqUnit.assertEquals("The action should not have been fired again.", 1, inputMapper.model.count);
60 | }, 500);
61 | });
62 | });
63 |
64 | fluid.each(["axes", "buttons"], function (inputType) {
65 | jqUnit.test("Repeating " + inputType + " actions should repeat.", function () {
66 | var inputMapper = gamepad.test.repeatRate.inputMapper();
67 |
68 | jqUnit.assertEquals("No actions should have been executed on startup.", 0, inputMapper.model.count);
69 |
70 | inputMapper.applier.change([inputType, 1], 1);
71 |
72 | jqUnit.assertEquals("The initial " + inputType + " press should have fired an action.", 1, inputMapper.model.count);
73 |
74 | jqUnit.stop();
75 |
76 | setTimeout(function () {
77 | jqUnit.start();
78 |
79 | jqUnit.assertEquals("The action should have been fired again.", 2, inputMapper.model.count);
80 | }, 500);
81 | });
82 | });
83 |
84 |
85 | fluid.each(["axes", "buttons"], function (inputType) {
86 | jqUnit.test("Analog " + inputType + " should work properly with non-repeating actions.", function () {
87 | var inputMapper = gamepad.test.repeatRate.inputMapper();
88 |
89 | jqUnit.assertEquals("No actions should have been executed on startup.", 0, inputMapper.model.count);
90 |
91 | inputMapper.applier.change([inputType, 0], 1);
92 |
93 | jqUnit.assertEquals("The initial " + inputType + " press should have fired an action.", 1, inputMapper.model.count);
94 |
95 | inputMapper.applier.change([inputType, 0], 0.75);
96 |
97 | jqUnit.assertEquals("A different " + inputType + " value above the threshold should not fire an action.", 1, inputMapper.model.count);
98 |
99 | jqUnit.stop();
100 |
101 | setTimeout(function () {
102 | jqUnit.start();
103 |
104 | jqUnit.assertEquals("The action should not have repeated.", 1, inputMapper.model.count);
105 | }, 500);
106 | });
107 | });
108 |
109 | fluid.each(["axes", "buttons"], function (inputType) {
110 | jqUnit.test("Analog " + inputType + " should work properly with repeating actions.", function () {
111 | var inputMapper = gamepad.test.repeatRate.inputMapper();
112 |
113 | jqUnit.assertEquals("No actions should have been executed on startup.", 0, inputMapper.model.count);
114 |
115 | inputMapper.applier.change([inputType, 1], 1);
116 |
117 | jqUnit.assertEquals("The initial input should have fired an action.", 1, inputMapper.model.count);
118 |
119 | inputMapper.applier.change([inputType, 1], 0.75);
120 |
121 | jqUnit.assertEquals("A different analog value above the threshold should not fire an action.", 1, inputMapper.model.count);
122 |
123 | jqUnit.stop();
124 |
125 | setTimeout(function () {
126 | jqUnit.start();
127 |
128 | jqUnit.assertEquals("The action should eventually have been repeated.", 2, inputMapper.model.count);
129 | }, 500);
130 | });
131 | });
132 | })(fluid);
133 |
--------------------------------------------------------------------------------
/src/js/content_scripts/modalManager.js:
--------------------------------------------------------------------------------
1 | /*
2 | Copyright (c) 2023 The Gamepad Navigator Authors
3 | See the AUTHORS.md file at the top-level directory of this distribution and at
4 | https://github.com/fluid-lab/gamepad-navigator/raw/main/AUTHORS.md.
5 |
6 | Licensed under the BSD 3-Clause License. You may not use this file except in
7 | compliance with this License.
8 |
9 | You may obtain a copy of the BSD 3-Clause License at
10 | https://github.com/fluid-lab/gamepad-navigator/blob/main/LICENSE
11 | */
12 |
13 | (function (fluid) {
14 | "use strict";
15 |
16 | var gamepad = fluid.registerNamespace("gamepad");
17 |
18 |
19 | fluid.defaults("gamepad.modalManager", {
20 | gradeNames: ["gamepad.shadowHolder"],
21 | markup: {
22 | container: ""
23 | },
24 | model: {
25 | activeModal: false,
26 | fullscreen: false,
27 | lastExternalFocused: false,
28 | inputValue: "",
29 | inputType: ""
30 | },
31 | components: {
32 | actionLauncher: {
33 | container: "{that}.model.shadowElement",
34 | type: "gamepad.actionLauncher",
35 | createOnEvent: "onShadowReady",
36 | options: {
37 | model: {
38 | hidden: "{gamepad.modalManager}.model.hideActionLauncher",
39 | prefs: "{gamepad.modalManager}.model.prefs"
40 | }
41 | }
42 | },
43 | onscreenKeyboard: {
44 | container: "{that}.model.shadowElement",
45 | type: "gamepad.osk.modal",
46 | createOnEvent: "onShadowReady",
47 | options: {
48 | model: {
49 | hidden: "{gamepad.modalManager}.model.hideOnscreenKeyboard",
50 | prefs: "{gamepad.modalManager}.model.prefs",
51 | lastExternalFocused: "{gamepad.modalManager}.model.lastExternalFocused",
52 | inputValue: "{gamepad.modalManager}.model.inputValue"
53 | }
54 | }
55 | },
56 | onscreenNumpad: {
57 | container: "{that}.model.shadowElement",
58 | type: "gamepad.numpad.modal",
59 | createOnEvent: "onShadowReady",
60 | options: {
61 | model: {
62 | hidden: "{gamepad.modalManager}.model.hideOnscreenNumpad",
63 | prefs: "{gamepad.modalManager}.model.prefs",
64 | inputValue: "{gamepad.modalManager}.model.inputValue"
65 | }
66 | }
67 | },
68 | searchKeyboard: {
69 | container: "{that}.model.shadowElement",
70 | type: "gamepad.searchKeyboard.modal",
71 | createOnEvent: "onShadowReady",
72 | options: {
73 | model: {
74 | hidden: "{gamepad.modalManager}.model.hideSearchKeyboard",
75 | prefs: "{gamepad.modalManager}.model.prefs"
76 | }
77 | }
78 | },
79 | selectOperator: {
80 | container: "{that}.model.shadowElement",
81 | type: "gamepad.selectOperator",
82 | createOnEvent: "onShadowReady",
83 | options: {
84 | model: {
85 | hidden: "{gamepad.modalManager}.model.hideSelectOperator",
86 | prefs: "{gamepad.modalManager}.model.prefs",
87 | selectElement: "{gamepad.modalManager}.model.selectElement"
88 | }
89 | }
90 | }
91 | },
92 | modelListeners: {
93 | activeModal: {
94 | excludeSource: "init",
95 | funcName: "gamepad.modalManager.toggleModals",
96 | args: ["{that}"]
97 | },
98 | fullscreen: {
99 | excludeSource: "init",
100 | funcName: "gamepad.modalManager.reattachToDOM",
101 | args: ["{that}"]
102 | }
103 | }
104 | });
105 |
106 | gamepad.modalManager.reattachToDOM = function (that) {
107 | that.applier.change("activeModal", false);
108 |
109 | var toAttach = document.fullscreenElement || document.body;
110 | toAttach.appendChild(that.container[0]);
111 | };
112 |
113 | gamepad.modalManager.toggleModals = function (that) {
114 | var transaction = that.applier.initiate();
115 | var hideActionLauncher = that.model.activeModal !== "actionLauncher";
116 | transaction.fireChangeRequest({ path: "hideActionLauncher", value: hideActionLauncher });
117 |
118 | var hideOnscreenKeyboard = that.model.activeModal !== "onscreenKeyboard";
119 | transaction.fireChangeRequest({ path: "hideOnscreenKeyboard", value: hideOnscreenKeyboard });
120 |
121 | var hideSearchKeyboard = that.model.activeModal !== "searchKeyboard";
122 | transaction.fireChangeRequest({ path: "hideSearchKeyboard", value: hideSearchKeyboard });
123 |
124 | var hideSelectOperator = that.model.activeModal !== "selectOperator";
125 | transaction.fireChangeRequest({ path: "hideSelectOperator", value: hideSelectOperator });
126 |
127 | var hideOnscreenNumpad = that.model.activeModal !== "onscreenNumpad";
128 | transaction.fireChangeRequest({ path: "hideOnscreenNumpad", value: hideOnscreenNumpad});
129 |
130 | transaction.commit();
131 | };
132 | })(fluid);
133 |
--------------------------------------------------------------------------------
/src/js/content_scripts/select-operator.js:
--------------------------------------------------------------------------------
1 | /*
2 | Copyright (c) 2023 The Gamepad Navigator Authors
3 | See the AUTHORS.md file at the top-level directory of this distribution and at
4 | https://github.com/fluid-lab/gamepad-navigator/raw/main/AUTHORS.md.
5 |
6 | Licensed under the BSD 3-Clause License. You may not use this file except in
7 | compliance with this License.
8 |
9 | You may obtain a copy of the BSD 3-Clause License at
10 | https://github.com/fluid-lab/gamepad-navigator/blob/main/LICENSE
11 | */
12 | (function (fluid) {
13 | "use strict";
14 | var gamepad = fluid.registerNamespace("gamepad");
15 |
16 | fluid.defaults("gamepad.selectOperator", {
17 | gradeNames: ["gamepad.modal"],
18 | model: {
19 | classNames: " selectOperator-modal",
20 | closeButtonLabel: "Cancel",
21 | label: "Gamepad Navigator: Select Option",
22 | selectElement: false,
23 | items: {}
24 | },
25 |
26 | modelListeners: {
27 | selectElement: {
28 | funcName: "gamepad.selectOperator.generateItemsFromSelect",
29 | args: ["{that}"]
30 | }
31 | },
32 |
33 | invokers: {
34 | handleItemClick: {
35 | funcName: "gamepad.selectOperator.handleItemClick",
36 | args: ["{that}", "{arguments}.0", "{arguments}.1"] // nodeIndex, event
37 | }
38 | },
39 |
40 | components: {
41 | listSelector: {
42 | type: "gamepad.ui.listSelector",
43 | container: "{that}.dom.modalBody",
44 | options: {
45 | model: {
46 | description: "Select an option from the list below or hit "Cancel" to close this modal. Only selectable items are displayed.",
47 | items: "{gamepad.selectOperator}.model.items",
48 | prefs: "{gamepad.selectOperator}.model.prefs"
49 | },
50 | invokers: {
51 | handleItemClick: {
52 | func: "{gamepad.selectOperator}.handleItemClick",
53 | args: ["{arguments}.0", "{arguments}.1"] // nodeIndex, event
54 | }
55 | }
56 | }
57 | }
58 | }
59 | });
60 |
61 | gamepad.selectOperator.generateItemsFromSelect = function (that) {
62 | var selectElement = fluid.get(that.model, "selectElement");
63 |
64 | if (selectElement) {
65 | var generatedItems = {};
66 |
67 | for (var nodeIndex = 0; nodeIndex < selectElement.children.length; nodeIndex++) {
68 | var childNode = selectElement.children.item(nodeIndex);
69 | var isHidden = childNode.getAttribute("hidden") !== null ? true : false;
70 | var isDisabled = childNode.getAttribute("disabled") !== null ? true : false;
71 | var isSelected = childNode.getAttribute("selected") !== null ? true : false;
72 |
73 | if (childNode.nodeName === "OPTION" && !isHidden && !isDisabled) {
74 | generatedItems[nodeIndex] = {
75 | description: childNode.textContent,
76 | selected: isSelected
77 | };
78 | }
79 | }
80 |
81 | var transaction = that.applier.initiate();
82 | transaction.fireChangeRequest({ path: "items", type: "DELETE" });
83 | transaction.fireChangeRequest({ path: "items", value: generatedItems });
84 | transaction.commit();
85 | }
86 | };
87 |
88 | gamepad.selectOperator.handleItemClick = function (that, nodeIndexString, event) {
89 | var nodeIndex = parseInt(nodeIndexString);
90 |
91 | var selectElement = fluid.get(that, "model.selectElement");
92 | if (selectElement) {
93 | if (selectElement.multiple) {
94 | // Toggle the clicked option without affecting any other selected values.
95 | var clickedOptionElement = selectElement.children[nodeIndex];
96 | if (clickedOptionElement.selected) {
97 | clickedOptionElement.removeAttribute("selected");
98 | }
99 | else {
100 | clickedOptionElement.setAttribute("selected", "");
101 | }
102 | }
103 | else {
104 | for (var optionIndex = 0; optionIndex < selectElement.children.length; optionIndex++) {
105 | var optionElement = selectElement.children[optionIndex];
106 | // Only select the option that was clicked.
107 | if (optionIndex === nodeIndex) {
108 | var optionValue = optionElement.value;
109 |
110 | // Just setting the value doesn't property trigger a change.
111 | $(selectElement).val(optionValue);
112 |
113 | optionElement.setAttribute("selected", "");
114 | }
115 | // Clear the selected attribute for the rest.
116 | else {
117 | optionElement.removeAttribute("selected");
118 | }
119 | }
120 | }
121 | selectElement.dispatchEvent(new Event("change"));
122 | }
123 |
124 | var transaction = that.applier.initiate();
125 | transaction.fireChangeRequest({ path: "items", type: "DELETE" });
126 | transaction.fireChangeRequest({ path: "items", value: {} });
127 | transaction.fireChangeRequest({ path: "selectElement", value: false });
128 | transaction.commit();
129 |
130 | that.closeModal(event);
131 | selectElement.focus();
132 | };
133 | })(fluid);
134 |
--------------------------------------------------------------------------------
/src/js/content_scripts/focus-overlay.js:
--------------------------------------------------------------------------------
1 | (function (fluid) {
2 | "use strict";
3 |
4 | var gamepad = fluid.registerNamespace("gamepad");
5 |
6 | fluid.defaults("gamepad.focusOverlay.pointer", {
7 | gradeNames: ["gamepad.templateRenderer"],
8 | markup: {
9 | container: ""
10 | },
11 | listeners: {
12 | "onCreate.listenForWindowFocusEvents": {
13 | funcName: "gamepad.focusOverlay.pointer.listenForWindowFocusEvents",
14 | args: ["{that}"]
15 | }
16 | },
17 | invokers: {
18 | handleFocusin: {
19 | funcName: "gamepad.focusOverlay.pointer.handleFocusin",
20 | args: ["{that}", "{arguments}.0"] // event
21 | }
22 | },
23 | modelListeners: {
24 | modalManagerShadowElement: {
25 | funcName: "gamepad.focusOverlay.pointer.listenForModalFocusEvents",
26 | args: ["{that}"]
27 | }
28 | }
29 | });
30 |
31 | gamepad.focusOverlay.pointer.listenForWindowFocusEvents = function (that) {
32 | window.addEventListener("focusin", that.handleFocusin);
33 | };
34 |
35 | gamepad.focusOverlay.pointer.listenForModalFocusEvents = function (that) {
36 | var modalManagerShadowElement = fluid.get(that, "model.modalManagerShadowElement");
37 | if (modalManagerShadowElement) {
38 | modalManagerShadowElement.addEventListener("focusin", that.handleFocusin);
39 | }
40 | };
41 |
42 | gamepad.focusOverlay.pointer.handleFocusin = function (that) {
43 | var containerDomElement = that.container[0];
44 |
45 | var activeElement = fluid.get(that.model, "modalManagerShadowElement.activeElement") || document.activeElement;
46 |
47 | var clientRect = activeElement.getBoundingClientRect();
48 |
49 | // Our outline is three pixels, so we adjust everything accordingly.
50 | containerDomElement.style.left = (clientRect.x + window.scrollX - 3) + "px";
51 | containerDomElement.style.top = (clientRect.y + window.scrollY - 3) + "px";
52 | containerDomElement.style.height = (clientRect.height) + "px";
53 | containerDomElement.style.width = (clientRect.width) + "px";
54 |
55 | var elementStyles = getComputedStyle(activeElement);
56 | var borderRadiusValue = elementStyles.getPropertyValue("border-radius");
57 | if (borderRadiusValue.length) {
58 | containerDomElement.style.borderRadius = borderRadiusValue;
59 | }
60 | else {
61 | containerDomElement.style.borderRadius = 0;
62 | }
63 | };
64 |
65 | fluid.defaults("gamepad.focusOverlay", {
66 | gradeNames: ["gamepad.shadowHolder"],
67 | markup: {
68 | container: ""
69 | },
70 | model: {
71 | shadowElement: false,
72 |
73 | modalManagerShadowElement: false,
74 |
75 | prefs: {},
76 | hideFocusOverlay: true
77 | },
78 | modelRelay: {
79 | hideFocusOverlay: {
80 | source: "{that}.model.hideFocusOverlay",
81 | target: "{that}.model.dom.container.attr.hidden"
82 | }
83 | },
84 | components: {
85 | pointer: {
86 | container: "{that}.model.shadowElement",
87 | type: "gamepad.focusOverlay.pointer",
88 | createOnEvent: "onShadowReady",
89 | options: {
90 | model: {
91 | modalManagerShadowElement: "{gamepad.focusOverlay}.model.modalManagerShadowElement"
92 | }
93 | }
94 | }
95 | },
96 | modelListeners: {
97 | prefs: {
98 | excludeSource: "init",
99 | funcName: "gamepad.focusOverlay.shouldDisplayOverlay",
100 | args: ["{that}"]
101 | },
102 | modalManagerShadowElement: {
103 | funcName: "gamepad.focusOverlay.listenForModalFocusEvents",
104 | args: ["{that}"]
105 | }
106 | },
107 | listeners: {
108 | "onCreate.listenForWindowFocusEvents": {
109 | funcName: "gamepad.focusOverlay.listenForWindowFocusEvents",
110 | args: ["{that}"]
111 | }
112 | },
113 | invokers: {
114 | shouldDisplayOverlay: {
115 | funcName: "gamepad.focusOverlay.shouldDisplayOverlay",
116 | args: ["{that}", "{arguments}.0"] // event
117 | }
118 | }
119 | });
120 |
121 | gamepad.focusOverlay.shouldDisplayOverlay = function (that) {
122 | var activeElement = fluid.get(that.model, "modalManagerShadowElement.activeElement") || document.activeElement;
123 | var fixFocus = fluid.get(that, "model.prefs.fixFocus") ? true : false;
124 | var hideFocusOverlay = !fixFocus || !activeElement;
125 | that.applier.change("hideFocusOverlay", hideFocusOverlay);
126 | };
127 |
128 | gamepad.focusOverlay.listenForWindowFocusEvents = function (that) {
129 | window.addEventListener("focusin", that.shouldDisplayOverlay);
130 | window.addEventListener("focusout", that.shouldDisplayOverlay);
131 | };
132 |
133 | gamepad.focusOverlay.listenForModalFocusEvents = function (that) {
134 | var modalManagerShadowElement = fluid.get(that, "model.modalManagerShadowElement");
135 | if (modalManagerShadowElement) {
136 | modalManagerShadowElement.addEventListener("focusin", that.shouldDisplayOverlay);
137 | modalManagerShadowElement.addEventListener("focusout", that.shouldDisplayOverlay);
138 | }
139 | };
140 | })(fluid);
141 |
--------------------------------------------------------------------------------
/src/images/button.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
114 |
--------------------------------------------------------------------------------
/tests/js/arrows/arrows-tests.js:
--------------------------------------------------------------------------------
1 | /*
2 | Copyright (c) 2023 The Gamepad Navigator Authors
3 | See the AUTHORS.md file at the top-level directory of this distribution and at
4 | https://github.com/fluid-lab/gamepad-navigator/raw/main/AUTHORS.md.
5 |
6 | Licensed under the BSD 3-Clause License. You may not use this file except in
7 | compliance with this License.
8 |
9 | You may obtain a copy of the BSD 3-Clause License at
10 | https://github.com/fluid-lab/gamepad-navigator/blob/main/LICENSE
11 | */
12 |
13 | /* global gamepad, jqUnit */
14 |
15 | (function (fluid, $) {
16 | "use strict";
17 |
18 | $(document).ready(function () {
19 |
20 | fluid.registerNamespace("gamepad.tests");
21 |
22 | var counter;
23 | jqUnit.module("Gamepad Navigator Arrow Tests", {
24 | setup: function () {
25 | counter = gamepad.tests.utils.arrows.counter("body");
26 | },
27 | teardown: function () {
28 | counter.destroy();
29 | }
30 | });
31 |
32 | var arrowButtons = {
33 | 12: "ArrowUp",
34 | 13: "ArrowDown",
35 | 14: "ArrowLeft",
36 | 15: "ArrowRight"
37 | };
38 |
39 | fluid.each(arrowButtons, function (keyCode, button) {
40 | jqUnit.test("Arrow button tests: " + keyCode, function () {
41 | counter.focus();
42 | var inputMapper = gamepad.tests.arrows.buttonInputMapper();
43 |
44 | var initialKeydownValue = fluid.get(counter, ["model", "eventCount", "keydown", keyCode ]);
45 | jqUnit.assertUndefined("There should be no initial keydown counter value for " + keyCode, initialKeydownValue);
46 |
47 | var initialKeyupValue = fluid.get(counter, ["model", "eventCount", "keyup", keyCode ]);
48 | jqUnit.assertUndefined("There should be no initial keyup counter value for " + keyCode, initialKeyupValue);
49 |
50 | inputMapper.applier.change(["buttons", button], 1);
51 |
52 | var updatedKeydownValue = fluid.get(counter, ["model", "eventCount", "keydown", keyCode ]);
53 | jqUnit.assertEquals("The keydown counter value should have been updated for " + keyCode, 1, updatedKeydownValue);
54 |
55 | var updatedKeyupValue = fluid.get(counter, ["model", "eventCount", "keyup", keyCode ]);
56 | jqUnit.assertEquals("The keydown counter value should have been updated for " + keyCode, 1, updatedKeyupValue);
57 | });
58 | });
59 |
60 | var axisTestDefs = {
61 | "Horizontal Axis": {
62 | invert: false,
63 | axis: 0,
64 | negativeKey: "ArrowLeft",
65 | positiveKey: "ArrowRight"
66 | },
67 | "Inverted Horizontal Axis": {
68 | invert: true,
69 | axis: 0,
70 | negativeKey: "ArrowRight",
71 | positiveKey: "ArrowLeft"
72 | },
73 | "Vertical Axis": {
74 | invert: false,
75 | axis: 1,
76 | negativeKey: "ArrowUp",
77 | positiveKey: "ArrowDown"
78 | },
79 | "Inverted Vertical Axis": {
80 | invert: true,
81 | axis: 1,
82 | negativeKey: "ArrowDown",
83 | positiveKey: "ArrowUp"
84 | }
85 | };
86 |
87 | fluid.each(axisTestDefs, function (testDef, testDefKey) {
88 | jqUnit.test(testDefKey, function () {
89 | var negativeModelPath = ["model", "eventCount", "keydown", testDef.negativeKey];
90 | var positiveModelPath = ["model", "eventCount", "keydown", testDef.positiveKey];
91 |
92 | counter.focus();
93 | var inputMapper = gamepad.tests.axisInputMapper({
94 | invert: testDef.invert
95 | });
96 |
97 | var initialNegativeValue = fluid.get(counter, negativeModelPath);
98 | jqUnit.assertUndefined("There should be no initial keydown counter value for the negative side of the axis.", initialNegativeValue);
99 |
100 | var initialPositiveValue = fluid.get(counter, positiveModelPath);
101 | jqUnit.assertUndefined("There should be no initial keydown counter value for the positive side of the axis.", initialPositiveValue);
102 |
103 | inputMapper.applier.change(["axes", testDef.axis], -1);
104 |
105 | jqUnit.stop();
106 | setTimeout(function () {
107 | jqUnit.start();
108 | var secondNegativeValue = fluid.get(counter, negativeModelPath);
109 | jqUnit.assertEquals("An arrow should have been sent from the negative side of the axis.", 1, secondNegativeValue);
110 |
111 | var secondPositiveValue = fluid.get(counter, positiveModelPath);
112 | jqUnit.assertUndefined("An arrow should not have been sent from the positive side of the axis.", secondPositiveValue);
113 |
114 | inputMapper.applier.change(["axes", testDef.axis], 0);
115 | inputMapper.applier.change(["axes", testDef.axis], 1);
116 |
117 | jqUnit.stop();
118 | setTimeout(function () {
119 | jqUnit.start();
120 | var thirdNegativeValue = fluid.get(counter, negativeModelPath);
121 | jqUnit.assertEquals("No additional arrows should have been sent from the negative side of the axis.", 1, thirdNegativeValue);
122 |
123 | var finalPositiveValue = fluid.get(counter, positiveModelPath);
124 | jqUnit.assertEquals("An arrow should have been sent from the positive side of the axis.", 1, finalPositiveValue);
125 | }, 50);
126 | }, 50);
127 | });
128 | });
129 | });
130 | })(fluid, jQuery);
131 |
--------------------------------------------------------------------------------
/docs/PUBLISHING.md:
--------------------------------------------------------------------------------
1 |
12 |
13 | # Publishing to the Chrome Web Store
14 |
15 | 1. Prepare the code and the `master` branch.
16 | 1. Update the `version` number in the [manifest](src/manifest.json#L3) and [package](package.json#L3) files.
17 | 1. The project follows [semantic versioning](https://semver.org/), i.e. the version is in the format
18 | "{MAJOR.MINOR.PATCH}[-dev]". For example, "1.0.15" and "1.1.9-dev".
19 | 1. The `version` number in [manifest.json](src/manifest.json) file should be completely numeric. However, other
20 | json files (for example, [package.json](package.json)) may contain alphabets and non-numeric characters with
21 | the version number.
22 | (See [Manifest Version](https://developer.chrome.com/extensions/manifest/version) for more details)
23 | 2. If the release is a **dev release**, use one of the following commands.
24 | 1. For bug fixes with no API or functionality changes, update the PATCH version: `grunt updateVersion --patch`
25 | 2. For minor API changes and functionality updates, update the MINOR version: `grunt updateVersion --minor`
26 | 3. For breaking API changes and new functionalities, update the MAJOR version: `grunt updateVersion --major`
27 | 3. For a **standard release**, add the command-line flag `--standard` to the above commands.
28 | 1. Dev release with PATCH version updates: `grunt updateVersion --patch --standard`
29 | 2. Dev release with MINOR version updates: `grunt updateVersion --minor --standard`
30 | 3. Dev release with MAJOR version updates: `grunt updateVersion --major --standard`
31 | 4. For a transition from a **dev release** to a **standard release** without updating the version number, use the
32 | command: `grunt updateVersion --standard`
33 | 5. Push the changes to the `master` branch.
34 | 2. Ensure that all of the code that should be published has been merged into the `master` branch.
35 | 3. Ensure that the code in the `master` branch is working as expected.
36 | 1. Lint: `npm run lint`
37 | 2. Run tests: `npm test`
38 | 3. Manual test build.
39 | 1. Create a build and load the generated unpacked extension into Chrome.
40 | (Refer to the [Installation](../README.md#installation) section for more details)
41 | 2. Test all of the available configuration options (Action, Speed Factor, et cetera) and ensure that they work
42 | on the browser correctly.
43 | 1. Refresh any browser tabs/windows that were open before installing the extension.
44 | 2. The actions and the associated configuration options should be tested individually and in combination to
45 | ensure that they are working correctly. For example, testing ***Scroll Vertically*** separately, then
46 | changing its ***Speed Factor***, and selecting the ***Invert Action*** option.
47 | 3. Test the same action and the associated configuration options with at least one different button,
48 | thumbstick, and trigger.
49 | 4. Multiple web pages should be tested to ensure that everything works correctly.
50 |
51 | 2. Create the release package: `grunt archive`
52 | 1. This will generate a zip file with the name "build_{tag_name}.zip" (for example, build_v0.1.0-dev.zip), which
53 | will be uploaded to the [Chrome Web Store](https://chrome.google.com/webstore/category/extensions).
54 |
55 | 3. Publish to the [Chrome Web Store](https://chrome.google.com/webstore/category/extensions).
56 | 1. Go to the Developer Dashboard on the Chrome Web Store and log in.
57 | 2. On the Developer Dashboard, click "Edit" (located on the Gamepad Navigator's right-hand side).
58 | 3. On the Gamepad Navigator edit page, click "Upload updated package" and upload the zip created in step 2 above.
59 | 4. Update the "Detailed description" field as necessary. Similar information is contained in this README.
60 | 5. Update the screenshots and videos if necessary. For example, if the images, icons, or the UI of the configuration
61 | panel has been changed. They will need to be the exact size requested.
62 | 6. Click "Preview Changes".
63 | 1. Verify that everything appears correct. Pay particular attention to anything that was changed. For example,
64 | version number/name, description, screenshots, et cetera.
65 | 7. If everything appears correct, publish the changes.
66 | 1. The actual publishing to the Chrome Web Store will take some time and may need to go through a review process.
67 |
68 | 4. Verify the published Gamepad Navigator Chrome extension.
69 | 1. Ensure that the contents of the Gamepad Navigator on the Chrome Web Store appear correct. Double-check the
70 | details like version number/name, descriptions, screenshots, et cetera.
71 | 2. Install the updated extension from the Chrome Web Store, and run through the manual testing again. (See step
72 | 1.3.3 above)
73 | 3. If there are any issues, fix them and repeat the process.
74 |
75 | 5. Create a GitHub release.
76 | 1. Set up the `.gruntfile.env` file.
77 | 1. Generate your GitHub access token.
78 | (Refer to the [GitHub Docs](https://tinyurl.com/yxsbzjme) for more details)
79 | 2. Create a copy of the `.gruntfile.env.default` file at the root level and rename the copy as `.gruntfile.env`.
80 | 3. Open the `.gruntfile.env` file and set the value of the `GITHUB_ACCESS_TOKEN` variable as your GitHub token
81 | generated in the above step.
82 | 2. Create a release on GitHub: `grunt release`
83 | 1. This will prompt the user to enter various details (release title, tag version, description, et cetera) on
84 | their terminal, and will create a new release as per those details.
85 | 3. Once the release is created successfully, announce it where required (for example, the fluid-work mailing list).
86 |
--------------------------------------------------------------------------------
/src/js/content_scripts/list-selector.js:
--------------------------------------------------------------------------------
1 | /*
2 | Copyright (c) 2023 The Gamepad Navigator Authors
3 | See the AUTHORS.md file at the top-level directory of this distribution and at
4 | https://github.com/fluid-lab/gamepad-navigator/raw/main/AUTHORS.md.
5 |
6 | Licensed under the BSD 3-Clause License. You may not use this file except in
7 | compliance with this License.
8 |
9 | You may obtain a copy of the BSD 3-Clause License at
10 | https://github.com/fluid-lab/gamepad-navigator/blob/main/LICENSE
11 | */
12 |
13 | (function (fluid) {
14 | "use strict";
15 | var gamepad = fluid.registerNamespace("gamepad");
16 |
17 | fluid.defaults("gamepad.ui.listSelector", {
18 | gradeNames: ["gamepad.templateRenderer"],
19 | markup: {
20 | container: "