summaryrefslogtreecommitdiffstats
path: root/toolkit/components/narrate/VoiceSelect.jsm
diff options
context:
space:
mode:
authorMatt A. Tobin <mattatobin@localhost.localdomain>2018-02-02 04:16:08 -0500
committerMatt A. Tobin <mattatobin@localhost.localdomain>2018-02-02 04:16:08 -0500
commit5f8de423f190bbb79a62f804151bc24824fa32d8 (patch)
tree10027f336435511475e392454359edea8e25895d /toolkit/components/narrate/VoiceSelect.jsm
parent49ee0794b5d912db1f95dce6eb52d781dc210db5 (diff)
downloadUXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar
UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar.gz
UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar.lz
UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar.xz
UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.zip
Add m-esr52 at 52.6.0
Diffstat (limited to 'toolkit/components/narrate/VoiceSelect.jsm')
-rw-r--r--toolkit/components/narrate/VoiceSelect.jsm299
1 files changed, 299 insertions, 0 deletions
diff --git a/toolkit/components/narrate/VoiceSelect.jsm b/toolkit/components/narrate/VoiceSelect.jsm
new file mode 100644
index 000000000..b283a06b3
--- /dev/null
+++ b/toolkit/components/narrate/VoiceSelect.jsm
@@ -0,0 +1,299 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const Cu = Components.utils;
+
+this.EXPORTED_SYMBOLS = ["VoiceSelect"];
+
+function VoiceSelect(win, label) {
+ this._winRef = Cu.getWeakReference(win);
+
+ let element = win.document.createElement("div");
+ element.classList.add("voiceselect");
+ element.innerHTML =
+ `<button class="select-toggle" aria-controls="voice-options">
+ <span class="label">${label}</span> <span class="current-voice"></span>
+ </button>
+ <div class="options" id="voice-options" role="listbox"></div>`;
+
+ this._elementRef = Cu.getWeakReference(element);
+
+ let button = this.selectToggle;
+ button.addEventListener("click", this);
+ button.addEventListener("keypress", this);
+
+ let listbox = this.listbox;
+ listbox.addEventListener("click", this);
+ listbox.addEventListener("mousemove", this);
+ listbox.addEventListener("keypress", this);
+ listbox.addEventListener("wheel", this, true);
+
+ win.addEventListener("resize", () => {
+ this._updateDropdownHeight();
+ });
+}
+
+VoiceSelect.prototype = {
+ add: function(label, value) {
+ let option = this._doc.createElement("button");
+ option.dataset.value = value;
+ option.classList.add("option");
+ option.tabIndex = "-1";
+ option.setAttribute("role", "option");
+ option.textContent = label;
+ this.listbox.appendChild(option);
+ return option;
+ },
+
+ addOptions: function(options) {
+ let selected = null;
+ for (let option of options) {
+ if (option.selected) {
+ selected = this.add(option.label, option.value);
+ } else {
+ this.add(option.label, option.value);
+ }
+ }
+
+ this._select(selected || this.options[0], true);
+ },
+
+ clear: function() {
+ this.listbox.innerHTML = "";
+ },
+
+ toggleList: function(force, focus = true) {
+ if (this.element.classList.toggle("open", force)) {
+ if (focus) {
+ (this.selected || this.options[0]).focus();
+ }
+
+ this._updateDropdownHeight(true);
+ this.listbox.setAttribute("aria-expanded", true);
+ this._win.addEventListener("focus", this, true);
+ } else {
+ if (focus) {
+ this.element.querySelector(".select-toggle").focus();
+ }
+
+ this.listbox.setAttribute("aria-expanded", false);
+ this._win.removeEventListener("focus", this, true);
+ }
+ },
+
+ handleEvent: function(evt) {
+ let target = evt.target;
+
+ switch (evt.type) {
+ case "click":
+ if (target.classList.contains("option")) {
+ if (!target.classList.contains("selected")) {
+ this.selected = target;
+ }
+
+ this.toggleList(false);
+ } else if (target.classList.contains("select-toggle")) {
+ this.toggleList();
+ }
+ break;
+
+ case "mousemove":
+ this.listbox.classList.add("hovering");
+ break;
+
+ case "keypress":
+ if (target.classList.contains("select-toggle")) {
+ if (evt.altKey) {
+ this.toggleList(true);
+ } else {
+ this._keyPressedButton(evt);
+ }
+ } else {
+ this.listbox.classList.remove("hovering");
+ this._keyPressedInBox(evt);
+ }
+ break;
+
+ case "wheel":
+ // Don't let wheel events bubble to document. It will scroll the page
+ // and close the entire narrate dialog.
+ evt.stopPropagation();
+ break;
+
+ case "focus":
+ if (!target.closest(".voiceselect")) {
+ this.toggleList(false, false);
+ }
+ break;
+ }
+ },
+
+ _getPagedOption: function(option, up) {
+ let height = elem => elem.getBoundingClientRect().height;
+ let listboxHeight = height(this.listbox);
+
+ let next = option;
+ for (let delta = 0; delta < listboxHeight; delta += height(next)) {
+ let sibling = up ? next.previousElementSibling : next.nextElementSibling;
+ if (!sibling) {
+ break;
+ }
+
+ next = sibling;
+ }
+
+ return next;
+ },
+
+ _keyPressedButton: function(evt) {
+ if (evt.altKey && (evt.key === "ArrowUp" || evt.key === "ArrowUp")) {
+ this.toggleList(true);
+ return;
+ }
+
+ let toSelect;
+ switch (evt.key) {
+ case "PageUp":
+ case "ArrowUp":
+ toSelect = this.selected.previousElementSibling;
+ break;
+ case "PageDown":
+ case "ArrowDown":
+ toSelect = this.selected.nextElementSibling;
+ break;
+ case "Home":
+ toSelect = this.selected.parentNode.firstElementChild;
+ break;
+ case "End":
+ toSelect = this.selected.parentNode.lastElementChild;
+ break;
+ }
+
+ if (toSelect && toSelect.classList.contains("option")) {
+ evt.preventDefault();
+ this.selected = toSelect;
+ }
+ },
+
+ _keyPressedInBox: function(evt) {
+ let toFocus;
+ let cur = this._doc.activeElement;
+
+ switch (evt.key) {
+ case "ArrowUp":
+ toFocus = cur.previousElementSibling || this.listbox.lastElementChild;
+ break;
+ case "ArrowDown":
+ toFocus = cur.nextElementSibling || this.listbox.firstElementChild;
+ break;
+ case "PageUp":
+ toFocus = this._getPagedOption(cur, true);
+ break;
+ case "PageDown":
+ toFocus = this._getPagedOption(cur, false);
+ break;
+ case "Home":
+ toFocus = cur.parentNode.firstElementChild;
+ break;
+ case "End":
+ toFocus = cur.parentNode.lastElementChild;
+ break;
+ case "Escape":
+ this.toggleList(false);
+ break;
+ }
+
+ if (toFocus && toFocus.classList.contains("option")) {
+ evt.preventDefault();
+ toFocus.focus();
+ }
+ },
+
+ _select: function(option, suppressEvent = false) {
+ let oldSelected = this.selected;
+ if (oldSelected) {
+ oldSelected.removeAttribute("aria-selected");
+ oldSelected.classList.remove("selected");
+ }
+
+ if (option) {
+ option.setAttribute("aria-selected", true);
+ option.classList.add("selected");
+ this.element.querySelector(".current-voice").textContent =
+ option.textContent;
+ }
+
+ if (!suppressEvent) {
+ let evt = this.element.ownerDocument.createEvent("Event");
+ evt.initEvent("change", true, true);
+ this.element.dispatchEvent(evt);
+ }
+ },
+
+ _updateDropdownHeight: function(now) {
+ let updateInner = () => {
+ let winHeight = this._win.innerHeight;
+ let listbox = this.listbox;
+ let listboxTop = listbox.getBoundingClientRect().top;
+ listbox.style.maxHeight = (winHeight - listboxTop - 10) + "px";
+ };
+
+ if (now) {
+ updateInner();
+ } else if (!this._pendingDropdownUpdate) {
+ this._pendingDropdownUpdate = true;
+ this._win.requestAnimationFrame(() => {
+ updateInner();
+ delete this._pendingDropdownUpdate;
+ });
+ }
+ },
+
+ _getOptionFromValue: function(value) {
+ return Array.from(this.options).find(o => o.dataset.value === value);
+ },
+
+ get element() {
+ return this._elementRef.get();
+ },
+
+ get listbox() {
+ return this._elementRef.get().querySelector(".options");
+ },
+
+ get selectToggle() {
+ return this._elementRef.get().querySelector(".select-toggle");
+ },
+
+ get _win() {
+ return this._winRef.get();
+ },
+
+ get _doc() {
+ return this._win.document;
+ },
+
+ set selected(option) {
+ this._select(option);
+ },
+
+ get selected() {
+ return this.element.querySelector(".options > .option.selected");
+ },
+
+ get options() {
+ return this.element.querySelectorAll(".options > .option");
+ },
+
+ set value(value) {
+ this._select(this._getOptionFromValue(value));
+ },
+
+ get value() {
+ let selected = this.selected;
+ return selected ? selected.dataset.value : "";
+ }
+};