You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@myfaces.apache.org by hn...@apache.org on 2025/04/15 08:33:03 UTC

(myfaces-tobago) 02/03: fix(selectOneList): deselection

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

hnoeth pushed a commit to branch tobago-5.x
in repository https://gitbox.apache.org/repos/asf/myfaces-tobago.git

commit 6f1b67ff03a6eca9486949e476872591c48f0a6f
Author: Henning Noeth <hn...@apache.org>
AuthorDate: Mon Apr 14 10:31:13 2025 +0200

    fix(selectOneList): deselection
    
    * add test
    * deselection is now possible like it is for the selectOneListbox component
    * adjust unit test
    
    Issue: TOBAGO-2403
---
 .../renderkit/renderer/SelectOneListRenderer.java  |   1 +
 .../renderer/selectOneList/selectOneListAjax.html  |   2 +-
 .../demo/test/onelist/DeselectController.java      |  90 +++++++++++++++++++
 .../2098-selectOneList/deselect/Deselect.test.js   | 100 +++++++++++++++++++++
 .../2098-selectOneList/deselect/Deselect.xhtml     |  42 +++++++++
 .../src/main/ts/tobago-select-one-list.ts          |   6 +-
 6 files changed, 237 insertions(+), 4 deletions(-)

diff --git a/tobago-core/src/main/java/org/apache/myfaces/tobago/internal/renderkit/renderer/SelectOneListRenderer.java b/tobago-core/src/main/java/org/apache/myfaces/tobago/internal/renderkit/renderer/SelectOneListRenderer.java
index 114caa8d2e..f2280f6ab5 100644
--- a/tobago-core/src/main/java/org/apache/myfaces/tobago/internal/renderkit/renderer/SelectOneListRenderer.java
+++ b/tobago-core/src/main/java/org/apache/myfaces/tobago/internal/renderkit/renderer/SelectOneListRenderer.java
@@ -84,6 +84,7 @@ private void encodeHiddenSelect(
     writer.writeAttribute(HtmlAttributes.DISABLED, disabled);
     writer.writeAttribute(HtmlAttributes.REQUIRED, component.isRequired());
     writer.writeClassAttribute(BootstrapClass.D_NONE);
+    writer.writeAttribute(HtmlAttributes.SIZE, 2);
 
     final String submittedValue = (String) component.getSubmittedValue();
     renderSelectItems(component, null, items, component.getValue(), submittedValue, writer, facesContext);
diff --git a/tobago-core/src/test/resources/renderer/selectOneList/selectOneListAjax.html b/tobago-core/src/test/resources/renderer/selectOneList/selectOneListAjax.html
index d29494144d..2666bdb22f 100644
--- a/tobago-core/src/test/resources/renderer/selectOneList/selectOneListAjax.html
+++ b/tobago-core/src/test/resources/renderer/selectOneList/selectOneListAjax.html
@@ -16,7 +16,7 @@
 -->
 
 <tobago-select-one-list id='id' class='tobago-auto-spacing'>
-<select id='id::field' name='id' class='d-none'>
+<select id='id::field' name='id' class='d-none' size='2'>
 <option value='Stratocaster'>Stratocaster</option>
 <option value='Telecaster'>Telecaster</option></select>
 <div class='dropdown'>
diff --git a/tobago-example/tobago-example-demo/src/main/java/org/apache/myfaces/tobago/example/demo/test/onelist/DeselectController.java b/tobago-example/tobago-example-demo/src/main/java/org/apache/myfaces/tobago/example/demo/test/onelist/DeselectController.java
new file mode 100644
index 0000000000..f493babb60
--- /dev/null
+++ b/tobago-example/tobago-example-demo/src/main/java/org/apache/myfaces/tobago/example/demo/test/onelist/DeselectController.java
@@ -0,0 +1,90 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.myfaces.tobago.example.demo.test.onelist;
+
+import org.apache.myfaces.tobago.example.demo.AstroData;
+import org.apache.myfaces.tobago.example.demo.SolarObject;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import javax.annotation.PostConstruct;
+import javax.enterprise.context.SessionScoped;
+import javax.inject.Inject;
+import javax.inject.Named;
+import java.io.Serializable;
+import java.lang.invoke.MethodHandles;
+import java.util.Date;
+import java.util.List;
+
+@Named
+@SessionScoped
+public class DeselectController implements Serializable {
+
+  private static final Logger LOG = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
+
+  @Inject
+  private AstroData astroData;
+
+  private List<SolarObject> planets;
+  private SolarObject selectedPlanet;
+  private boolean required = false;
+  private boolean expanded = true;
+
+  @PostConstruct
+  public void init() {
+    planets = astroData.getSatellites("Sun");
+  }
+
+  public void reset() {
+    selectedPlanet = null;
+  }
+
+  public List<SolarObject> getPlanets() {
+    return planets;
+  }
+
+  public SolarObject getSelectedPlanet() {
+    return selectedPlanet;
+  }
+
+  public void setSelectedPlanet(SolarObject selectedPlanet) {
+    this.selectedPlanet = selectedPlanet;
+  }
+
+  public boolean isRequired() {
+    return required;
+  }
+
+  public void setRequired(boolean required) {
+    this.required = required;
+  }
+
+  public boolean isExpanded() {
+    return expanded;
+  }
+
+  public void setExpanded(boolean expanded) {
+    this.expanded = expanded;
+  }
+
+  public long getCurrentTimestamp() {
+    return new Date().getTime();
+  }
+}
diff --git a/tobago-example/tobago-example-demo/src/main/webapp/content/900-test/2098-selectOneList/deselect/Deselect.test.js b/tobago-example/tobago-example-demo/src/main/webapp/content/900-test/2098-selectOneList/deselect/Deselect.test.js
new file mode 100644
index 0000000000..1faf23945a
--- /dev/null
+++ b/tobago-example/tobago-example-demo/src/main/webapp/content/900-test/2098-selectOneList/deselect/Deselect.test.js
@@ -0,0 +1,100 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import {JasmineTestTool} from "/tobago/test/tobago-test-tool.js";
+import {elementByIdFn, querySelectorFn} from "/script/tobago-test.js";
+
+it("deselection if required=false", function (done) {
+  const hiddenSelect = elementByIdFn("page:mainForm:selectOneList::field");
+  const spanSelected = querySelectorFn("#page\\:mainForm\\:selectOneList\\:\\:selectField span");
+  const saturnRow = querySelectorFn(".tobago-options[name='page:mainForm:selectOneList'] tr[data-tobago-value='Saturn']");
+  const output = querySelectorFn("#page\\:mainForm\\:output .form-control-plaintext");
+  const submit = elementByIdFn("page:mainForm:submit");
+  const reset = elementByIdFn("page:mainForm:reset");
+  const requiredCheckbox = elementByIdFn("page:mainForm:requiredCheckbox::field");
+
+  let timestamp;
+
+  const test = new JasmineTestTool(done);
+  test.setup(() => hiddenSelect().required === false,
+      () => requiredCheckbox().checked = false, "change", requiredCheckbox);
+  test.setup(() => spanSelected().textContent === "" && output().textContent === "", null, "click", reset);
+  test.do(() => expect(hiddenSelect().required).toBeFalse());
+  test.do(() => expect(spanSelected().textContent).toEqual(""));
+  test.do(() => expect(output().textContent).toEqual(""));
+
+  test.do(() => clickRow(saturnRow));
+  test.waitMs(1000);
+  test.do(() => expect(spanSelected().textContent).toEqual("Saturn"));
+  test.do(() => expect(output().textContent).toEqual(""));
+
+  test.do(() => clickRow(saturnRow));
+  test.waitMs(1000);
+  test.do(() => expect(spanSelected().textContent).toEqual(""));
+  test.do(() => expect(output().textContent).toEqual(""));
+
+  test.do(() => timestamp = currentTimestamp());
+  test.event("click", submit, () => currentTimestamp() > timestamp);
+  test.do(() => expect(spanSelected().textContent).toEqual(""));
+  test.do(() => expect(output().textContent).toEqual(""));
+  test.start();
+});
+
+it("deselection if required=true", function (done) {
+  const hiddenSelect = elementByIdFn("page:mainForm:selectOneList::field");
+  const spanSelected = querySelectorFn("#page\\:mainForm\\:selectOneList\\:\\:selectField span");
+  const jupiterRow = querySelectorFn(".tobago-options[name='page:mainForm:selectOneList'] tr[data-tobago-value='Jupiter']");
+  const output = querySelectorFn("#page\\:mainForm\\:output .form-control-plaintext");
+  const submit = elementByIdFn("page:mainForm:submit");
+  const reset = elementByIdFn("page:mainForm:reset");
+  const requiredCheckbox = elementByIdFn("page:mainForm:requiredCheckbox::field");
+
+  let timestamp;
+
+  const test = new JasmineTestTool(done);
+  test.setup(() => hiddenSelect().required === true,
+      () => requiredCheckbox().checked = true, "change", requiredCheckbox);
+  test.setup(() => spanSelected().textContent === "" && output().textContent === "", null, "click", reset);
+  test.do(() => expect(hiddenSelect().required).toBeTrue());
+  test.do(() => expect(spanSelected().textContent).toEqual(""));
+  test.do(() => expect(output().textContent).toEqual(""));
+
+  test.do(() => clickRow(jupiterRow));
+  test.waitMs(1000);
+  test.do(() => expect(spanSelected().textContent).toEqual("Jupiter"));
+  test.do(() => expect(output().textContent).toEqual(""));
+
+  test.do(() => clickRow(jupiterRow));
+  test.waitMs(1000);
+  test.do(() => expect(spanSelected().textContent).toEqual("Jupiter"));
+  test.do(() => expect(output().textContent).toEqual(""));
+
+  test.do(() => timestamp = currentTimestamp());
+  test.event("click", submit, () => currentTimestamp() > timestamp);
+  test.do(() => expect(spanSelected().textContent).toEqual("Jupiter"));
+  test.do(() => expect(output().textContent).toEqual("Jupiter"));
+  test.start();
+});
+
+function clickRow(row) {
+  row().dispatchEvent(new Event("click", {bubbles: true}))
+}
+
+function currentTimestamp() {
+  const timestampOut = querySelectorFn("#page\\:mainForm\\:timestamp .form-control-plaintext");
+  return Number(timestampOut().textContent);
+}
diff --git a/tobago-example/tobago-example-demo/src/main/webapp/content/900-test/2098-selectOneList/deselect/Deselect.xhtml b/tobago-example/tobago-example-demo/src/main/webapp/content/900-test/2098-selectOneList/deselect/Deselect.xhtml
new file mode 100644
index 0000000000..54ef24b5b8
--- /dev/null
+++ b/tobago-example/tobago-example-demo/src/main/webapp/content/900-test/2098-selectOneList/deselect/Deselect.xhtml
@@ -0,0 +1,42 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<!--
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+-->
+
+<ui:composition template="/main.xhtml"
+                xmlns="http://www.w3.org/1999/xhtml"
+                xmlns:f="http://xmlns.jcp.org/jsf/core"
+                xmlns:tc="http://myfaces.apache.org/tobago/component"
+                xmlns:ui="http://xmlns.jcp.org/jsf/facelets">
+
+  <tc:selectOneList id="selectOneList" label="SelectOneList" value="#{deselectController.selectedPlanet}"
+                    required="#{deselectController.required}"
+                    expanded="#{deselectController.expanded}">
+    <tc:selectItems value="#{deselectController.planets}"
+                    var="planet" itemLabel="#{planet.name}" itemValue="#{planet}"/>
+  </tc:selectOneList>
+  <tc:out id="output" label="Planet" value="#{deselectController.selectedPlanet}"/>
+  <tc:button id="submit" label="Submit"/>
+  <tc:button id="reset" label="Reset" action="#{deselectController.reset()}"/>
+  <tc:selectBooleanCheckbox id="requiredCheckbox" value="#{deselectController.required}" itemLabel="required">
+    <f:ajax render="selectOneList timestamp"/>
+  </tc:selectBooleanCheckbox>
+  <tc:selectBooleanCheckbox id="expandedCheckbox" value="#{deselectController.expanded}" itemLabel="expanded">
+    <f:ajax render="selectOneList timestamp"/>
+  </tc:selectBooleanCheckbox>
+  <tc:out id="timestamp" label="Timestamp" value="#{deselectController.currentTimestamp}"/>
+</ui:composition>
diff --git a/tobago-theme/tobago-theme-standard/src/main/ts/tobago-select-one-list.ts b/tobago-theme/tobago-theme-standard/src/main/ts/tobago-select-one-list.ts
index 337fafbfd2..d589a979a4 100644
--- a/tobago-theme/tobago-theme-standard/src/main/ts/tobago-select-one-list.ts
+++ b/tobago-theme/tobago-theme-standard/src/main/ts/tobago-select-one-list.ts
@@ -79,16 +79,16 @@ class SelectOneList extends SelectListBase {
   protected select(row: HTMLTableRowElement): void {
     const itemValue = row.dataset.tobagoValue;
     const option: HTMLOptionElement = this.hiddenSelect.querySelector(`[value="${itemValue}"]`);
-    option.selected = true;
+    option.selected = this.hiddenSelect.required ? true : !option.selected;
     this.filterInput.value = null;
     this.sync();
     this.hiddenSelect.dispatchEvent(new Event("change", {bubbles: true}));
   }
 
   private sync() {
+    this.spanText = this.selectedOption?.textContent;
     this.rows.forEach((row) => {
       if (row.dataset.tobagoValue === this.hiddenSelect.value) {
-        this.spanText = this.selectedOption.textContent;
         row.classList.add(Css.TABLE_PRIMARY); // highlight list row
       } else {
         row.classList.remove(Css.TABLE_PRIMARY); // remove highlight list row
@@ -100,7 +100,7 @@ class SelectOneList extends SelectListBase {
     this.focused = false;
     this.filterInput.value = null;
     this.filterInput.dispatchEvent(new Event("input"));
-    this.spanText = this.selectedOption.textContent;
+    this.spanText = this.selectedOption?.textContent;
     this.dropdownMenu?.hide();
   }