You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@myfaces.apache.org by lo...@apache.org on 2020/06/24 09:31:43 UTC

[myfaces-tobago] 03/04: tobago-focus: custom elements

This is an automated email from the ASF dual-hosted git repository.

lofwyr pushed a commit to branch jsf23+quarkus
in repository https://gitbox.apache.org/repos/asf/myfaces-tobago.git

commit 2ca08e18ff2789c389e99a96b429644a2e72c3cb
Author: Henning Nöth <hn...@apache.org>
AuthorDate: Thu Jun 4 21:58:23 2020 +0200

    tobago-focus: custom elements
    
    Added tobago-focus web element.
    The focusListener to set the lastFocusId must be implemented in the appropriate web elements. For this, several web elements are implemented or improved.
    The executing of [autofocus] within a popup is now handled in the tobago-popup web element.
    
    Issue: TOBAGO-1633
---
 .../internal/renderkit/renderer/PageRenderer.java  |   8 +-
 .../renderer/SelectManyListboxRenderer.java        |   2 +-
 .../renderer/SelectOneChoiceRenderer.java          |   2 +-
 .../renderkit/renderer/TextareaRenderer.java       |   2 +-
 .../tobago/renderkit/html/HtmlElements.java        |   4 +
 tobago-core/src/main/resources/scss/_tobago.scss   |  13 ++
 .../src/main/npm/ts/tobago-all.ts                  |   4 +
 .../src/main/npm/ts/tobago-focus.ts                | 144 ++++++++++++---------
 .../src/main/npm/ts/tobago-in.ts                   |  19 +++
 .../src/main/npm/ts/tobago-popup.ts                |   6 +
 .../main/npm/ts/tobago-select-boolean-checkbox.ts  |   3 +
 .../src/main/npm/ts/tobago-select-many-checkbox.ts |   4 +
 ...y-checkbox.ts => tobago-select-many-listbox.ts} |  25 +---
 .../src/main/npm/ts/tobago-select-many-shuttle.ts  |   5 +
 ...ean-checkbox.ts => tobago-select-one-choice.ts} |  17 +--
 ...an-checkbox.ts => tobago-select-one-listbox.ts} |  17 +--
 .../src/main/npm/ts/tobago-select-one-radio.ts     |   3 +
 ...lect-boolean-checkbox.ts => tobago-textarea.ts} |  17 +--
 18 files changed, 170 insertions(+), 125 deletions(-)

diff --git a/tobago-core/src/main/java/org/apache/myfaces/tobago/internal/renderkit/renderer/PageRenderer.java b/tobago-core/src/main/java/org/apache/myfaces/tobago/internal/renderkit/renderer/PageRenderer.java
index 21fa0ab..f570c41 100644
--- a/tobago-core/src/main/java/org/apache/myfaces/tobago/internal/renderkit/renderer/PageRenderer.java
+++ b/tobago-core/src/main/java/org/apache/myfaces/tobago/internal/renderkit/renderer/PageRenderer.java
@@ -280,12 +280,16 @@ public class PageRenderer extends RendererBase {
     writer.writeAttribute(HtmlAttributes.DISABLED, true);
     writer.endElement(HtmlElements.INPUT);
 
+    final String lastFocusId = clientId + ComponentUtils.SUB_SEPARATOR + "lastFocusId";
+    writer.startElement(HtmlElements.TOBAGO_FOCUS);
+    writer.writeIdAttribute(lastFocusId);
     writer.startElement(HtmlElements.INPUT);
     writer.writeAttribute(HtmlAttributes.TYPE, HtmlInputTypes.HIDDEN);
-    writer.writeNameAttribute(clientId + ComponentUtils.SUB_SEPARATOR + "lastFocusId");
-    writer.writeIdAttribute(clientId + ComponentUtils.SUB_SEPARATOR + "lastFocusId");
+    writer.writeNameAttribute(lastFocusId);
+    writer.writeIdAttribute(lastFocusId + ComponentUtils.SUB_SEPARATOR + "field");
     writer.writeAttribute(HtmlAttributes.VALUE, tobagoContext.getFocusId(), true);
     writer.endElement(HtmlElements.INPUT);
+    writer.endElement(HtmlElements.TOBAGO_FOCUS);
 
     if (tobagoConfig.isCheckSessionSecret()) {
       writer.startElement(HtmlElements.INPUT);
diff --git a/tobago-core/src/main/java/org/apache/myfaces/tobago/internal/renderkit/renderer/SelectManyListboxRenderer.java b/tobago-core/src/main/java/org/apache/myfaces/tobago/internal/renderkit/renderer/SelectManyListboxRenderer.java
index a2d431a..5bc4c6d 100644
--- a/tobago-core/src/main/java/org/apache/myfaces/tobago/internal/renderkit/renderer/SelectManyListboxRenderer.java
+++ b/tobago-core/src/main/java/org/apache/myfaces/tobago/internal/renderkit/renderer/SelectManyListboxRenderer.java
@@ -40,7 +40,7 @@ public class SelectManyListboxRenderer extends SelectManyRendererBase {
 
   @Override
   public HtmlElements getComponentTag() {
-    return HtmlElements.DIV;
+    return HtmlElements.TOBAGO_SELECT_MANY_LISTBOX;
   }
 
   @Override
diff --git a/tobago-core/src/main/java/org/apache/myfaces/tobago/internal/renderkit/renderer/SelectOneChoiceRenderer.java b/tobago-core/src/main/java/org/apache/myfaces/tobago/internal/renderkit/renderer/SelectOneChoiceRenderer.java
index 86f49da..4e3c7a0 100644
--- a/tobago-core/src/main/java/org/apache/myfaces/tobago/internal/renderkit/renderer/SelectOneChoiceRenderer.java
+++ b/tobago-core/src/main/java/org/apache/myfaces/tobago/internal/renderkit/renderer/SelectOneChoiceRenderer.java
@@ -40,7 +40,7 @@ public class SelectOneChoiceRenderer extends SelectOneRendererBase {
 
   @Override
   public HtmlElements getComponentTag() {
-    return HtmlElements.DIV;
+    return HtmlElements.TOBAGO_SELECT_ONE_CHOICE;
   }
 
   @Override
diff --git a/tobago-core/src/main/java/org/apache/myfaces/tobago/internal/renderkit/renderer/TextareaRenderer.java b/tobago-core/src/main/java/org/apache/myfaces/tobago/internal/renderkit/renderer/TextareaRenderer.java
index ff90914..5f42d3b 100644
--- a/tobago-core/src/main/java/org/apache/myfaces/tobago/internal/renderkit/renderer/TextareaRenderer.java
+++ b/tobago-core/src/main/java/org/apache/myfaces/tobago/internal/renderkit/renderer/TextareaRenderer.java
@@ -50,7 +50,7 @@ public class TextareaRenderer extends MessageLayoutRendererBase {
 
   @Override
   public HtmlElements getComponentTag() {
-    return HtmlElements.DIV;
+    return HtmlElements.TOBAGO_TEXTAREA;
   }
 
   private static final Logger LOG = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
diff --git a/tobago-core/src/main/java/org/apache/myfaces/tobago/renderkit/html/HtmlElements.java b/tobago-core/src/main/java/org/apache/myfaces/tobago/renderkit/html/HtmlElements.java
index 0cf0900..46304f7 100644
--- a/tobago-core/src/main/java/org/apache/myfaces/tobago/renderkit/html/HtmlElements.java
+++ b/tobago-core/src/main/java/org/apache/myfaces/tobago/renderkit/html/HtmlElements.java
@@ -138,6 +138,7 @@ public enum HtmlElements {
   TOBAGO_DATE("tobago-date"),
   TOBAGO_DROPDOWN("tobago-dropdown"),
   TOBAGO_FILE("tobago-file"),
+  TOBAGO_FOCUS("tobago-focus"),
   TOBAGO_FOOTER("tobago-footer"),
   TOBAGO_LABEL("tobago-label"),
   TOBAGO_HEADER("tobago-header"),
@@ -151,7 +152,9 @@ public enum HtmlElements {
   TOBAGO_SELECT_BOOLEAN_CHECKBOX("tobago-select-boolean-checkbox"),
   TOBAGO_SELECT_BOOLEAN_TOGGLE("tobago-select-boolean-toggle"),
   TOBAGO_SELECT_MANY_CHECKBOX("tobago-select-many-checkbox"),
+  TOBAGO_SELECT_MANY_LISTBOX("tobago-select-many-listbox"),
   TOBAGO_SELECT_MANY_SHUTTLE("tobago-select-many-shuttle"),
+  TOBAGO_SELECT_ONE_CHOICE("tobago-select-one-choice"),
   TOBAGO_SELECT_ONE_LISTBOX("tobago-select-one-listbox"),
   TOBAGO_SELECT_ONE_RADIO("tobago-select-one-radio"),
   TOBAGO_SHEET("tobago-sheet"),
@@ -160,6 +163,7 @@ public enum HtmlElements {
   TOBAGO_TAB("tobago-tab"),
   TOBAGO_TAB_CONTENT("tobago-tab-content"),
   TOBAGO_TAB_GROUP("tobago-tab-group"),
+  TOBAGO_TEXTAREA("tobago-textarea"),
   TOBAGO_TREE("tobago-tree"),
   TOBAGO_TREE_LISTBOX("tobago-tree-listbox"),
   TOBAGO_TREE_NODE("tobago-tree-node"),
diff --git a/tobago-core/src/main/resources/scss/_tobago.scss b/tobago-core/src/main/resources/scss/_tobago.scss
index e81e455..2fdb039 100644
--- a/tobago-core/src/main/resources/scss/_tobago.scss
+++ b/tobago-core/src/main/resources/scss/_tobago.scss
@@ -326,6 +326,11 @@ tobago-file {
 .tobago-flowLayout {
 }
 
+/* focus --------------------------------------------------------------- */
+tobago-focus {
+  display: none;
+}
+
 /* footer -------------------------------------------------------------- */
 
 tobago-footer {
@@ -1041,6 +1046,11 @@ tobago-select-boolean-toggle {
   @include adjustCustomControlLabel();
 }
 
+/* selectOneChoice --------------------------------------------------------- */
+tobago-select-one-choice {
+
+}
+
 /* selectOneListbox -------------------------------------------------------- */
 tobago-select-one-listbox {
   display: block;
@@ -1081,6 +1091,9 @@ tobago-select-many-checkbox {
 }
 
 /* selectManyListbox ----------------------------------------------------------- */
+tobago-select-many-listbox {
+
+}
 .tobago-selectManyListbox,
 .tobago-selectManyListbox-option {
 }
diff --git a/tobago-theme/tobago-theme-standard/src/main/npm/ts/tobago-all.ts b/tobago-theme/tobago-theme-standard/src/main/npm/ts/tobago-all.ts
index 99281d5..d87e181 100644
--- a/tobago-theme/tobago-theme-standard/src/main/npm/ts/tobago-all.ts
+++ b/tobago-theme/tobago-theme-standard/src/main/npm/ts/tobago-all.ts
@@ -38,13 +38,17 @@ import "./tobago-scroll";
 import "./tobago-select-boolean-checkbox";
 import "./tobago-select-boolean-toggle";
 import "./tobago-select-many-checkbox";
+import "./tobago-select-many-listbox";
 import "./tobago-select-many-shuttle";
+import "./tobago-select-one-choice";
+import "./tobago-select-one-listbox";
 import "./tobago-select-one-radio";
 import "./tobago-sheet";
 import "./tobago-split-layout";
 import "./tobago-stars";
 import "./tobago-suggest";
 import "./tobago-tab";
+import "./tobago-textarea";
 import "./tobago-tree";
 import "./tobago-tree-listbox";
 import "./tobago-tree-node";
diff --git a/tobago-theme/tobago-theme-standard/src/main/npm/ts/tobago-focus.ts b/tobago-theme/tobago-theme-standard/src/main/npm/ts/tobago-focus.ts
index eba35a2..beab956 100644
--- a/tobago-theme/tobago-theme-standard/src/main/npm/ts/tobago-focus.ts
+++ b/tobago-theme/tobago-theme-standard/src/main/npm/ts/tobago-focus.ts
@@ -15,22 +15,32 @@
  * limitations under the License.
  */
 
-import {DomUtils} from "./tobago-utils";
-import {Listener, Phase} from "./tobago-listener";
 import {Page} from "./tobago-page";
+import {DomUtils} from "./tobago-utils";
 
-export class Focus {
-
-  private static getHidden(): HTMLInputElement {
-    return document.getElementById(Page.page().id + DomUtils.SUB_COMPONENT_SEP + "lastFocusId") as HTMLInputElement;
-  }
+export class Focus extends HTMLElement {
 
-  static setLastFocusId(id: string): void {
-    this.getHidden().value = id;
+  /**
+   * The focusListener to set the lastFocusId must be implemented in the appropriate web elements.
+   * @param event
+   */
+  static setLastFocusId(event: FocusEvent): void {
+    const target = event.target as HTMLElement;
+    let computedStyle = getComputedStyle(target);
+
+    if (target.getAttribute("type") !== "hidden"
+        && target.getAttributeNames().indexOf("disabled") === -1
+        && target.getAttribute("tabindex") !== "-1"
+        && computedStyle.visibility !== "hidden"
+        && computedStyle.display !== "none") {
+      const root = target.getRootNode() as ShadowRoot | Document;
+      const tobagoFocus = root.getElementById(Page.page().id + DomUtils.SUB_COMPONENT_SEP + "lastFocusId");
+      tobagoFocus.querySelector("input").value = target.id;
+    }
   }
 
-  static getLastFocusId(): string {
-    return this.getHidden().value;
+  constructor() {
+    super();
   }
 
   /**
@@ -43,75 +53,85 @@ export class Focus {
    * - last (the element from the last request with same id gets the focus, not AJAX)
    * - first (the first input element (without tabindex=-1) gets the focus, not AJAX)
    */
-  static init = function (element: HTMLElement): void {
-
-    const activeInputs = Focus.activeInputs(element);
-
-    for (const focusable of activeInputs) {
-      focusable.addEventListener("focus", function (event: FocusEvent): void {
-        const target = event.target as HTMLElement;
-        if (target.style.visibility !== "hidden" && target.style.display != "none") {
-          // remember the last focused element, for later
-          Focus.setLastFocusId(target.id);
-        }
-      });
-    }
-
-    // autofocus in popups doesn't work automatically... so we fix that here
-    const modals = document.getElementsByClassName("modal");
-    for (let i = 0; i < modals.length; i++) {
-      modals.item(i).addEventListener("shown.bs.modal", Focus.initAutoFocus);
-    }
-
-    for (const hasDanger of DomUtils.selfOrElementsByClassName(element, ".has-danger")) {
-      const activeInputsInsideDanger = Focus.activeInputs(hasDanger);
-      if (activeInputsInsideDanger.length > 0) {
-        activeInputsInsideDanger[0].focus();
-        return;
-      }
+  connectedCallback(): void {
+    const errorElement = this.errorElement;
+    if (errorElement) {
+      errorElement.focus();
+      return;
     }
 
-    const autoFocus = element.querySelector("[autofocus]");
-    if (autoFocus) {
+    if (this.autofocusElements.length > 0) {
       // nothing to do, because the browser make the work.
       return;
     }
 
-    if (element.parentElement) {
-      // seems to be AJAX, so end here
+    const lastFocusedElement = this.lastFocusedElement;
+    if (lastFocusedElement) {
+      lastFocusedElement.focus();
       return;
     }
 
-    const lastFocusId = Focus.getLastFocusId();
-    if (lastFocusId) {
-      const element = document.getElementById(lastFocusId);
-      if (element) {
-        element.focus();
-      }
+    const focusableElement = this.focusableElement;
+    if (focusableElement) {
+      focusableElement.focus();
       return;
     }
+  }
 
-    if (activeInputs.length > 0) {
-      activeInputs[0].focus();
-      return;
+  private get errorElement(): HTMLElement {
+    const root = this.getRootNode() as ShadowRoot | Document;
+    const elements = root.querySelectorAll(
+        ".tobago-messages-container .border-danger:not([disabled]):not([tabindex='-1'])") as NodeListOf<HTMLElement>;
+
+    for (const element of elements) {
+      const computedStyle = getComputedStyle(element);
+      if (computedStyle.display !== "none" && computedStyle.visibility !== "hidden") {
+        return element;
+      }
     }
-  };
+  }
+
+  private get autofocusElements(): NodeListOf<HTMLElement> {
+    const root = this.getRootNode() as ShadowRoot | Document;
+    return root.querySelectorAll("[autofocus]");
+  }
+
+  private get lastFocusedElement(): HTMLElement {
+    const lastFocusId: string = this.hiddenInput.value;
+    const root = this.getRootNode() as ShadowRoot | Document;
+    return root.getElementById(lastFocusId);
+  }
+
+  private get hiddenInput(): HTMLInputElement {
+    return this.querySelector("input");
+  }
 
-  private static activeInputs(element: HTMLElement): Array<HTMLElement> {
-    return DomUtils.selfOrQuerySelectorAll(element,
+  private get focusableElement(): HTMLElement {
+    const root = this.getRootNode() as ShadowRoot | Document;
+    const elements = root.querySelectorAll(
         "input:not([type='hidden']):not([disabled]):not([tabindex='-1'])," +
         "select:not([disabled]):not([tabindex='-1'])," +
-        "textarea:not([disabled]):not([tabindex='-1'])")
-        .filter((element) =>
-            element.style.visibility !== "hidden"
-            && element.style.display != "none");
+        "textarea:not([disabled]):not([tabindex='-1'])") as NodeListOf<HTMLElement>;
+
+    for (const element of elements) {
+      if (this.isVisible(element)) {
+        return element;
+      }
+    }
   }
 
-  private static initAutoFocus(event): void {
-    const modal = event.currentTarget as HTMLElement;
-    (modal.querySelector("[autofocus]") as HTMLElement).focus();
+  private isVisible(element: HTMLElement): boolean {
+    const computedStyle = getComputedStyle(element);
+    if (computedStyle.display === "none" || computedStyle.visibility === "hidden") {
+      return false;
+    } else if (element.parentElement) {
+      return this.isVisible(element.parentElement);
+    } else {
+      return true;
+    }
   }
 }
 
-Listener.register(Focus.init, Phase.DOCUMENT_READY);
-Listener.register(Focus.init, Phase.AFTER_UPDATE);
+document.addEventListener("DOMContentLoaded", function (event: Event): void {
+  window.customElements.define("tobago-focus", Focus);
+});
diff --git a/tobago-theme/tobago-theme-standard/src/main/npm/ts/tobago-in.ts b/tobago-theme/tobago-theme-standard/src/main/npm/ts/tobago-in.ts
index 1c9f316..02c421d 100644
--- a/tobago-theme/tobago-theme-standard/src/main/npm/ts/tobago-in.ts
+++ b/tobago-theme/tobago-theme-standard/src/main/npm/ts/tobago-in.ts
@@ -17,6 +17,25 @@
 
 import {Listener, Phase} from "./tobago-listener";
 import {DomUtils} from "./tobago-utils";
+import {Focus} from "./tobago-focus";
+
+export class In extends HTMLElement {
+  constructor() {
+    super();
+  }
+
+  connectedCallback(): void {
+    this.input.addEventListener("focus", Focus.setLastFocusId);
+  }
+
+  get input(): HTMLInputElement {
+    return this.querySelector(DomUtils.escapeClientId(this.id + DomUtils.SUB_COMPONENT_SEP + "field"));
+  }
+}
+
+document.addEventListener("DOMContentLoaded", function (event: Event): void {
+  window.customElements.define("tobago-in", In);
+});
 
 // XXX regexp example only - blueprint
 class RegExpTest {
diff --git a/tobago-theme/tobago-theme-standard/src/main/npm/ts/tobago-popup.ts b/tobago-theme/tobago-theme-standard/src/main/npm/ts/tobago-popup.ts
index 7a215bf..0a86c80 100644
--- a/tobago-theme/tobago-theme-standard/src/main/npm/ts/tobago-popup.ts
+++ b/tobago-theme/tobago-theme-standard/src/main/npm/ts/tobago-popup.ts
@@ -312,6 +312,12 @@ export class Popup extends HTMLElement {
     // } else {
     //   transitionComplete()
     // }
+
+    this.dispatchEvent(new CustomEvent(Event.SHOWN));
+    const autofocusElement = this.querySelector("[autofocus]") as HTMLElement;
+    if (autofocusElement) {
+      autofocusElement.focus();
+    }
   }
 /*
   _enforceFocus() {
diff --git a/tobago-theme/tobago-theme-standard/src/main/npm/ts/tobago-select-boolean-checkbox.ts b/tobago-theme/tobago-theme-standard/src/main/npm/ts/tobago-select-boolean-checkbox.ts
index 8cd46ba..a6a3593 100644
--- a/tobago-theme/tobago-theme-standard/src/main/npm/ts/tobago-select-boolean-checkbox.ts
+++ b/tobago-theme/tobago-theme-standard/src/main/npm/ts/tobago-select-boolean-checkbox.ts
@@ -16,6 +16,7 @@
  */
 
 import {DomUtils} from "./tobago-utils";
+import {Focus} from "./tobago-focus";
 
 export class SelectBooleanCheckbox extends HTMLElement {
 
@@ -24,6 +25,8 @@ export class SelectBooleanCheckbox extends HTMLElement {
   }
 
   connectedCallback(): void {
+    this.input.addEventListener("focus", Focus.setLastFocusId);
+
     if (this.input.readOnly) {
       this.input.addEventListener("click", preventClick);
     }
diff --git a/tobago-theme/tobago-theme-standard/src/main/npm/ts/tobago-select-many-checkbox.ts b/tobago-theme/tobago-theme-standard/src/main/npm/ts/tobago-select-many-checkbox.ts
index 36b69e5..05fbc64 100644
--- a/tobago-theme/tobago-theme-standard/src/main/npm/ts/tobago-select-many-checkbox.ts
+++ b/tobago-theme/tobago-theme-standard/src/main/npm/ts/tobago-select-many-checkbox.ts
@@ -15,6 +15,8 @@
  * limitations under the License.
  */
 
+import {Focus} from "./tobago-focus";
+
 class SelectManyCheckbox extends HTMLElement {
 
   constructor() {
@@ -23,6 +25,8 @@ class SelectManyCheckbox extends HTMLElement {
 
   connectedCallback(): void {
     for (const input of this.inputs) {
+      input.addEventListener("focus", Focus.setLastFocusId);
+
       if (input.readOnly) {
         input.addEventListener("click", preventClick);
       }
diff --git a/tobago-theme/tobago-theme-standard/src/main/npm/ts/tobago-select-many-checkbox.ts b/tobago-theme/tobago-theme-standard/src/main/npm/ts/tobago-select-many-listbox.ts
similarity index 57%
copy from tobago-theme/tobago-theme-standard/src/main/npm/ts/tobago-select-many-checkbox.ts
copy to tobago-theme/tobago-theme-standard/src/main/npm/ts/tobago-select-many-listbox.ts
index 36b69e5..28cea87 100644
--- a/tobago-theme/tobago-theme-standard/src/main/npm/ts/tobago-select-many-checkbox.ts
+++ b/tobago-theme/tobago-theme-standard/src/main/npm/ts/tobago-select-many-listbox.ts
@@ -15,30 +15,11 @@
  * limitations under the License.
  */
 
-class SelectManyCheckbox extends HTMLElement {
+import {SelectOneListbox} from "./tobago-select-one-listbox";
 
-  constructor() {
-    super();
-  }
-
-  connectedCallback(): void {
-    for (const input of this.inputs) {
-      if (input.readOnly) {
-        input.addEventListener("click", preventClick);
-      }
-
-      function preventClick(event: MouseEvent): void {
-        // in the "readonly" case, prevent the default, which is changing the "checked" state
-        event.preventDefault();
-      }
-    }
-  }
-
-  get inputs(): NodeListOf<HTMLInputElement> {
-    return this.querySelectorAll("input[name='" + this.id + "']");
-  }
+class SelectManyListbox extends SelectOneListbox {
 }
 
 document.addEventListener("DOMContentLoaded", function (event: Event): void {
-  window.customElements.define("tobago-select-many-checkbox", SelectManyCheckbox);
+  window.customElements.define("tobago-select-many-listbox", SelectManyListbox);
 });
diff --git a/tobago-theme/tobago-theme-standard/src/main/npm/ts/tobago-select-many-shuttle.ts b/tobago-theme/tobago-theme-standard/src/main/npm/ts/tobago-select-many-shuttle.ts
index 1cf4161..2e8bd48 100644
--- a/tobago-theme/tobago-theme-standard/src/main/npm/ts/tobago-select-many-shuttle.ts
+++ b/tobago-theme/tobago-theme-standard/src/main/npm/ts/tobago-select-many-shuttle.ts
@@ -15,6 +15,8 @@
  * limitations under the License.
  */
 
+import {Focus} from "./tobago-focus";
+
 class SelectManyShuttle extends HTMLElement {
 
   constructor() {
@@ -22,6 +24,9 @@ class SelectManyShuttle extends HTMLElement {
   }
 
   connectedCallback(): void {
+    this.unselectedSelect.addEventListener("focus", Focus.setLastFocusId);
+    this.selectedSelect.addEventListener("focus", Focus.setLastFocusId);
+
     if (this.unselectedSelect.getAttribute("readonly") !== "readonly" && !this.unselectedSelect.disabled) {
       this.unselectedSelect.addEventListener("dblclick", this.addSelectedItems.bind(this));
     }
diff --git a/tobago-theme/tobago-theme-standard/src/main/npm/ts/tobago-select-boolean-checkbox.ts b/tobago-theme/tobago-theme-standard/src/main/npm/ts/tobago-select-one-choice.ts
similarity index 70%
copy from tobago-theme/tobago-theme-standard/src/main/npm/ts/tobago-select-boolean-checkbox.ts
copy to tobago-theme/tobago-theme-standard/src/main/npm/ts/tobago-select-one-choice.ts
index 8cd46ba..fff94da 100644
--- a/tobago-theme/tobago-theme-standard/src/main/npm/ts/tobago-select-boolean-checkbox.ts
+++ b/tobago-theme/tobago-theme-standard/src/main/npm/ts/tobago-select-one-choice.ts
@@ -16,29 +16,22 @@
  */
 
 import {DomUtils} from "./tobago-utils";
+import {Focus} from "./tobago-focus";
 
-export class SelectBooleanCheckbox extends HTMLElement {
-
+class SelectOneChoice extends HTMLElement {
   constructor() {
     super();
   }
 
   connectedCallback(): void {
-    if (this.input.readOnly) {
-      this.input.addEventListener("click", preventClick);
-    }
-
-    function preventClick(event: MouseEvent): void {
-      // in the "readonly" case, prevent the default, which is changing the "checked" state
-      event.preventDefault();
-    }
+    this.field.addEventListener("focus", Focus.setLastFocusId);
   }
 
-  get input(): HTMLInputElement {
+  get field(): HTMLInputElement {
     return this.querySelector(DomUtils.escapeClientId(this.id + DomUtils.SUB_COMPONENT_SEP + "field"));
   }
 }
 
 document.addEventListener("DOMContentLoaded", function (event: Event): void {
-  window.customElements.define("tobago-select-boolean-checkbox", SelectBooleanCheckbox);
+  window.customElements.define("tobago-select-one-choice", SelectOneChoice);
 });
diff --git a/tobago-theme/tobago-theme-standard/src/main/npm/ts/tobago-select-boolean-checkbox.ts b/tobago-theme/tobago-theme-standard/src/main/npm/ts/tobago-select-one-listbox.ts
similarity index 70%
copy from tobago-theme/tobago-theme-standard/src/main/npm/ts/tobago-select-boolean-checkbox.ts
copy to tobago-theme/tobago-theme-standard/src/main/npm/ts/tobago-select-one-listbox.ts
index 8cd46ba..8b3eac8 100644
--- a/tobago-theme/tobago-theme-standard/src/main/npm/ts/tobago-select-boolean-checkbox.ts
+++ b/tobago-theme/tobago-theme-standard/src/main/npm/ts/tobago-select-one-listbox.ts
@@ -16,29 +16,22 @@
  */
 
 import {DomUtils} from "./tobago-utils";
+import {Focus} from "./tobago-focus";
 
-export class SelectBooleanCheckbox extends HTMLElement {
-
+export class SelectOneListbox extends HTMLElement {
   constructor() {
     super();
   }
 
   connectedCallback(): void {
-    if (this.input.readOnly) {
-      this.input.addEventListener("click", preventClick);
-    }
-
-    function preventClick(event: MouseEvent): void {
-      // in the "readonly" case, prevent the default, which is changing the "checked" state
-      event.preventDefault();
-    }
+    this.field.addEventListener("focus", Focus.setLastFocusId);
   }
 
-  get input(): HTMLInputElement {
+  get field(): HTMLInputElement {
     return this.querySelector(DomUtils.escapeClientId(this.id + DomUtils.SUB_COMPONENT_SEP + "field"));
   }
 }
 
 document.addEventListener("DOMContentLoaded", function (event: Event): void {
-  window.customElements.define("tobago-select-boolean-checkbox", SelectBooleanCheckbox);
+  window.customElements.define("tobago-select-one-listbox", SelectOneListbox);
 });
diff --git a/tobago-theme/tobago-theme-standard/src/main/npm/ts/tobago-select-one-radio.ts b/tobago-theme/tobago-theme-standard/src/main/npm/ts/tobago-select-one-radio.ts
index bb2138f..b3a46e4 100644
--- a/tobago-theme/tobago-theme-standard/src/main/npm/ts/tobago-select-one-radio.ts
+++ b/tobago-theme/tobago-theme-standard/src/main/npm/ts/tobago-select-one-radio.ts
@@ -15,6 +15,8 @@
  * limitations under the License.
  */
 
+import {Focus} from "./tobago-focus";
+
 class SelectOneRadio extends HTMLElement {
 
   private oldCheckedId: string = "";
@@ -26,6 +28,7 @@ class SelectOneRadio extends HTMLElement {
   connectedCallback(): void {
     this.saveSelection();
     for (const radio of this.radioGroup) {
+      radio.addEventListener("focus", Focus.setLastFocusId);
       radio.addEventListener("click", this.clickSelection.bind(this));
     }
   }
diff --git a/tobago-theme/tobago-theme-standard/src/main/npm/ts/tobago-select-boolean-checkbox.ts b/tobago-theme/tobago-theme-standard/src/main/npm/ts/tobago-textarea.ts
similarity index 70%
copy from tobago-theme/tobago-theme-standard/src/main/npm/ts/tobago-select-boolean-checkbox.ts
copy to tobago-theme/tobago-theme-standard/src/main/npm/ts/tobago-textarea.ts
index 8cd46ba..fc522b5 100644
--- a/tobago-theme/tobago-theme-standard/src/main/npm/ts/tobago-select-boolean-checkbox.ts
+++ b/tobago-theme/tobago-theme-standard/src/main/npm/ts/tobago-textarea.ts
@@ -16,29 +16,22 @@
  */
 
 import {DomUtils} from "./tobago-utils";
+import {Focus} from "./tobago-focus";
 
-export class SelectBooleanCheckbox extends HTMLElement {
-
+class Textarea extends HTMLElement {
   constructor() {
     super();
   }
 
   connectedCallback(): void {
-    if (this.input.readOnly) {
-      this.input.addEventListener("click", preventClick);
-    }
-
-    function preventClick(event: MouseEvent): void {
-      // in the "readonly" case, prevent the default, which is changing the "checked" state
-      event.preventDefault();
-    }
+    this.textarea.addEventListener("focus", Focus.setLastFocusId);
   }
 
-  get input(): HTMLInputElement {
+  get textarea(): HTMLInputElement {
     return this.querySelector(DomUtils.escapeClientId(this.id + DomUtils.SUB_COMPONENT_SEP + "field"));
   }
 }
 
 document.addEventListener("DOMContentLoaded", function (event: Event): void {
-  window.customElements.define("tobago-select-boolean-checkbox", SelectBooleanCheckbox);
+  window.customElements.define("tobago-textarea", Textarea);
 });