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 2021/10/27 16:30:30 UTC

[myfaces-tobago] 01/02: refactor: clean up shuttle CSS class names

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 6a93e5b1d9aa17493e4b1aaad99c2027ea70843c
Author: Henning Noeth <hn...@apache.org>
AuthorDate: Wed Oct 27 16:50:44 2021 +0200

    refactor: clean up shuttle CSS class names
    
    * remove shuttle related css classes
    * remove unused tobago-hidden css class
    * fix: shuttle styling
    ** fix: message layout shadow
    * add tests
---
 .../renderer/SelectManyShuttleRenderer.java        | 41 ++++-----
 .../myfaces/tobago/renderkit/css/TobagoClass.java  | 28 ++++---
 .../internal/config/AbstractTobagoTestBase.java    |  5 ++
 .../SelectManyShuttleRendererUnitTest.java         | 98 ++++++++++++++++++++++
 .../renderer/selectManyShuttle/error-message.html  | 45 ++++++++++
 .../resources/renderer/selectManyShuttle/help.html | 45 ++++++++++
 .../renderer/selectManyShuttle/label.html          | 41 +++++++++
 .../renderer/selectManyShuttle/simple.html         | 40 +++++++++
 tobago-theme/src/main/scss/_tobago.scss            | 66 ++++-----------
 .../src/main/ts/tobago-select-many-shuttle.ts      | 14 ++--
 10 files changed, 330 insertions(+), 93 deletions(-)

diff --git a/tobago-core/src/main/java/org/apache/myfaces/tobago/internal/renderkit/renderer/SelectManyShuttleRenderer.java b/tobago-core/src/main/java/org/apache/myfaces/tobago/internal/renderkit/renderer/SelectManyShuttleRenderer.java
index 73f2820..e4a3027 100644
--- a/tobago-core/src/main/java/org/apache/myfaces/tobago/internal/renderkit/renderer/SelectManyShuttleRenderer.java
+++ b/tobago-core/src/main/java/org/apache/myfaces/tobago/internal/renderkit/renderer/SelectManyShuttleRenderer.java
@@ -54,7 +54,7 @@ public class SelectManyShuttleRenderer<T extends AbstractUISelectManyShuttle> ex
 
     writer.startElement(HtmlElements.DIV);
     writer.writeClassAttribute(
-        TobagoClass.SELECT_MANY_SHUTTLE,
+        TobagoClass.BODY,
         component.getCustomClass(),
         markup != null && markup.contains(Markup.SPREAD) ? TobagoClass.SPREAD : null);
     HtmlRendererUtils.writeDataAttributes(facesContext, writer, component);
@@ -70,7 +70,6 @@ public class SelectManyShuttleRenderer<T extends AbstractUISelectManyShuttle> ex
     final String unselectedLabel = component.getUnselectedLabel();
     if (unselectedLabel != null) {
       writer.startElement(HtmlElements.DIV);
-      writer.writeClassAttribute(TobagoClass.SELECT_MANY_SHUTTLE__UNSELECTED_LABEL);
       writer.write(unselectedLabel);
       writer.endElement(HtmlElements.DIV);
     }
@@ -86,33 +85,28 @@ public class SelectManyShuttleRenderer<T extends AbstractUISelectManyShuttle> ex
     // TODO tabIndex
     writer.writeAttribute(HtmlAttributes.TABINDEX, component.getTabIndex());
 
-    writer.writeClassAttribute(TobagoClass.SELECT_MANY_SHUTTLE__UNSELECTED, BootstrapClass.FORM_CONTROL);
+    writer.writeClassAttribute(TobagoClass.UNSELECTED, BootstrapClass.FORM_CONTROL);
 
     writer.writeAttribute(HtmlAttributes.MULTIPLE, true);
     writer.writeAttribute(HtmlAttributes.SIZE, size);
 
     final Object[] values = component.getSelectedValues();
     final String[] submittedValues = getSubmittedValues(component);
-    renderSelectItems(
-        component, TobagoClass.SELECT_MANY_SHUTTLE__OPTION, items, values, submittedValues, false, writer,
-        facesContext);
+    renderSelectItems(component, null, items, values, submittedValues, false, writer, facesContext);
 
     writer.endElement(HtmlElements.SELECT);
     writer.startElement(HtmlElements.DIV);
-    writer.writeClassAttribute(TobagoClass.SELECT_MANY_SHUTTLE__TOOL_BAR, BootstrapClass.BTN_GROUP_VERTICAL);
-    createButton(facesContext, component, writer, disabled | readonly,
-        Icons.CHEVRON_DOUBLE_RIGHT, "addAll", TobagoClass.SELECT_MANY_SHUTTLE__ADD_ALL);
-    createButton(facesContext, component, writer, disabled | readonly,
-        Icons.CHEVRON_RIGHT, "add", TobagoClass.SELECT_MANY_SHUTTLE__ADD);
-    createButton(facesContext, component, writer, disabled | readonly,
-        Icons.CHEVRON_LEFT, "remove", TobagoClass.SELECT_MANY_SHUTTLE__REMOVE);
-    createButton(facesContext, component, writer, disabled | readonly,
-        Icons.CHEVRON_DOUBLE_LEFT, "removeAll", TobagoClass.SELECT_MANY_SHUTTLE__REMOVE_ALL);
+    writer.startElement(HtmlElements.DIV);
+    writer.writeClassAttribute(BootstrapClass.BTN_GROUP_VERTICAL);
+    createButton(facesContext, component, writer, disabled | readonly, Icons.CHEVRON_DOUBLE_RIGHT, "addAll");
+    createButton(facesContext, component, writer, disabled | readonly, Icons.CHEVRON_RIGHT, "add");
+    createButton(facesContext, component, writer, disabled | readonly, Icons.CHEVRON_LEFT, "remove");
+    createButton(facesContext, component, writer, disabled | readonly, Icons.CHEVRON_DOUBLE_LEFT, "removeAll");
+    writer.endElement(HtmlElements.DIV);
     writer.endElement(HtmlElements.DIV);
     final String selectedLabel = component.getSelectedLabel();
     if (selectedLabel != null) {
       writer.startElement(HtmlElements.DIV);
-      writer.writeClassAttribute(TobagoClass.SELECT_MANY_SHUTTLE__SELECTED_LABEL);
       writer.write(selectedLabel);
       writer.endElement(HtmlElements.DIV);
     }
@@ -125,25 +119,22 @@ public class SelectManyShuttleRenderer<T extends AbstractUISelectManyShuttle> ex
     writer.writeAttribute(HtmlAttributes.READONLY, readonly);
     writer.writeAttribute(HtmlAttributes.TABINDEX, component.getTabIndex());
     writer.writeClassAttribute(
-        TobagoClass.SELECT_MANY_SHUTTLE__SELECTED,
+        TobagoClass.SELECTED,
         BootstrapClass.borderColor(ComponentUtils.getMaximumSeverity(component)),
         BootstrapClass.FORM_CONTROL);
     writer.writeAttribute(HtmlAttributes.MULTIPLE, true);
     writer.writeAttribute(HtmlAttributes.SIZE, size);
-    renderSelectItems(
-        component, TobagoClass.SELECT_MANY_SHUTTLE__OPTION, items, values, submittedValues, true, writer,
-        facesContext);
+    renderSelectItems(component, null, items, values, submittedValues, true, writer, facesContext);
 
     writer.endElement(HtmlElements.SELECT);
     writer.startElement(HtmlElements.SELECT);
-    writer.writeClassAttribute(TobagoClass.SELECT_MANY_SHUTTLE__HIDDEN);
+    writer.writeClassAttribute(BootstrapClass.D_NONE);
     final String hiddenClientId = clientId + ComponentUtils.SUB_SEPARATOR + "hidden";
     writer.writeIdAttribute(hiddenClientId);
     writer.writeNameAttribute(clientId);
     writer.writeAttribute(HtmlAttributes.MULTIPLE, true);
     writer.writeAttribute(HtmlAttributes.REQUIRED, component.isRequired());
-    renderSelectItems(
-        component, TobagoClass.SELECT_MANY_SHUTTLE__OPTION, items, values, submittedValues, writer, facesContext);
+    renderSelectItems(component, null, items, values, submittedValues, writer, facesContext);
     writer.endElement(HtmlElements.SELECT);
   }
 
@@ -157,10 +148,10 @@ public class SelectManyShuttleRenderer<T extends AbstractUISelectManyShuttle> ex
 
   private void createButton(
       final FacesContext context, final UIComponent component, final TobagoResponseWriter writer,
-      final boolean disabled, final Icons icon, final String sub, final TobagoClass cssClass) throws IOException {
+      final boolean disabled, final Icons icon, final String sub) throws IOException {
     writer.startElement(HtmlElements.BUTTON);
     writer.writeAttribute(HtmlAttributes.TYPE, HtmlButtonTypes.BUTTON);
-    writer.writeClassAttribute(cssClass, BootstrapClass.BTN, BootstrapClass.BTN_SECONDARY);
+    writer.writeClassAttribute(BootstrapClass.BTN, BootstrapClass.BTN_SECONDARY);
     writer.writeIdAttribute(component.getClientId(context) + ComponentUtils.SUB_SEPARATOR + sub);
     writer.writeAttribute(HtmlAttributes.DISABLED, disabled);
     writer.startElement(HtmlElements.I);
diff --git a/tobago-core/src/main/java/org/apache/myfaces/tobago/renderkit/css/TobagoClass.java b/tobago-core/src/main/java/org/apache/myfaces/tobago/renderkit/css/TobagoClass.java
index 8f5af1b..bf2f54f 100644
--- a/tobago-core/src/main/java/org/apache/myfaces/tobago/renderkit/css/TobagoClass.java
+++ b/tobago-core/src/main/java/org/apache/myfaces/tobago/renderkit/css/TobagoClass.java
@@ -30,6 +30,7 @@ public enum TobagoClass implements CssItem {
   AUTO__SPACING("tobago-auto-spacing"),
 //  BADGE("tobago-badge"),
   BAR("tobago-bar"),
+  BODY("tobago-body"),
   BOX__HEADER("tobago-box-header"),
   // tbd: can this be removed, when using <tobago-button>?
   BUTTON("tobago-button"),
@@ -68,18 +69,18 @@ public enum TobagoClass implements CssItem {
   REQUIRED("tobago-required"),
   SECTION__CONTENT("tobago-section-content"),
   SELECT_MANY_LISTBOX__OPTION("tobago-selectManyListbox-option"),
-  SELECT_MANY_SHUTTLE("tobago-selectManyShuttle"),
-  SELECT_MANY_SHUTTLE__ADD("tobago-selectManyShuttle-add"),
-  SELECT_MANY_SHUTTLE__ADD_ALL("tobago-selectManyShuttle-addAll"),
-  SELECT_MANY_SHUTTLE__HIDDEN("tobago-selectManyShuttle-hidden"),
-  SELECT_MANY_SHUTTLE__OPTION("tobago-selectManyShuttle-option"),
-  SELECT_MANY_SHUTTLE__REMOVE("tobago-selectManyShuttle-remove"),
-  SELECT_MANY_SHUTTLE__REMOVE_ALL("tobago-selectManyShuttle-removeAll"),
-  SELECT_MANY_SHUTTLE__SELECTED("tobago-selectManyShuttle-selected"),
-  SELECT_MANY_SHUTTLE__SELECTED_LABEL("tobago-selectManyShuttle-selectedLabel"),
-  SELECT_MANY_SHUTTLE__TOOL_BAR("tobago-selectManyShuttle-toolBar"),
-  SELECT_MANY_SHUTTLE__UNSELECTED("tobago-selectManyShuttle-unselected"),
-  SELECT_MANY_SHUTTLE__UNSELECTED_LABEL("tobago-selectManyShuttle-unselectedLabel"),
+//  SELECT_MANY_SHUTTLE("tobago-selectManyShuttle"),
+//  SELECT_MANY_SHUTTLE__ADD("tobago-selectManyShuttle-add"),
+//  SELECT_MANY_SHUTTLE__ADD_ALL("tobago-selectManyShuttle-addAll"),
+//  SELECT_MANY_SHUTTLE__HIDDEN("tobago-selectManyShuttle-hidden"),
+//  SELECT_MANY_SHUTTLE__OPTION("tobago-selectManyShuttle-option"),
+//  SELECT_MANY_SHUTTLE__REMOVE("tobago-selectManyShuttle-remove"),
+//  SELECT_MANY_SHUTTLE__REMOVE_ALL("tobago-selectManyShuttle-removeAll"),
+//  SELECT_MANY_SHUTTLE__SELECTED("tobago-selectManyShuttle-selected"),
+//  SELECT_MANY_SHUTTLE__SELECTED_LABEL("tobago-selectManyShuttle-selectedLabel"),
+//  SELECT_MANY_SHUTTLE__TOOL_BAR("tobago-selectManyShuttle-toolBar"),
+//  SELECT_MANY_SHUTTLE__UNSELECTED("tobago-selectManyShuttle-unselected"),
+//  SELECT_MANY_SHUTTLE__UNSELECTED_LABEL("tobago-selectManyShuttle-unselectedLabel"),
   SELECT_ONE_LISTBOX("tobago-selectOneListbox"),
   SELECT_ONE_LISTBOX__OPTION("tobago-selectOneListbox-option"),
   SELECTED("tobago-selected"),
@@ -119,10 +120,11 @@ public enum TobagoClass implements CssItem {
 //  TREE_LABEL("tobago-treeLabel"),
   TREE_LISTBOX("tobago-treeListbox"),
   TREE_LISTBOX__LEVEL("tobago-treeListbox-level"),
-  TREE_LISTBOX__SELECT("tobago-treeListbox-select");
+  TREE_LISTBOX__SELECT("tobago-treeListbox-select"),
 //  TREE_NODE("tobago-treeNode"),
 //  TREE_SELECT("tobago-treeSelect");
 //  TREE_SELECT__LABEL("tobago-treeSelect-label");
+  UNSELECTED("tobago-unselected");
 
   private final String name;
 
diff --git a/tobago-core/src/test/java/org/apache/myfaces/tobago/internal/config/AbstractTobagoTestBase.java b/tobago-core/src/test/java/org/apache/myfaces/tobago/internal/config/AbstractTobagoTestBase.java
index 685e658..a3b90b7 100644
--- a/tobago-core/src/test/java/org/apache/myfaces/tobago/internal/config/AbstractTobagoTestBase.java
+++ b/tobago-core/src/test/java/org/apache/myfaces/tobago/internal/config/AbstractTobagoTestBase.java
@@ -47,6 +47,7 @@ import org.apache.myfaces.tobago.component.UISelectBooleanToggle;
 import org.apache.myfaces.tobago.component.UISelectItem;
 import org.apache.myfaces.tobago.component.UISelectManyCheckbox;
 import org.apache.myfaces.tobago.component.UISelectManyListbox;
+import org.apache.myfaces.tobago.component.UISelectManyShuttle;
 import org.apache.myfaces.tobago.component.UISelectOneChoice;
 import org.apache.myfaces.tobago.component.UISelectOneRadio;
 import org.apache.myfaces.tobago.component.UISelectReference;
@@ -83,6 +84,7 @@ import org.apache.myfaces.tobago.internal.renderkit.renderer.SelectBooleanCheckb
 import org.apache.myfaces.tobago.internal.renderkit.renderer.SelectBooleanToggleRenderer;
 import org.apache.myfaces.tobago.internal.renderkit.renderer.SelectManyCheckboxRenderer;
 import org.apache.myfaces.tobago.internal.renderkit.renderer.SelectManyListboxRenderer;
+import org.apache.myfaces.tobago.internal.renderkit.renderer.SelectManyShuttleRenderer;
 import org.apache.myfaces.tobago.internal.renderkit.renderer.SelectOneChoiceRenderer;
 import org.apache.myfaces.tobago.internal.renderkit.renderer.SelectOneRadioRenderer;
 import org.apache.myfaces.tobago.internal.renderkit.renderer.SelectReferenceRenderer;
@@ -174,6 +176,7 @@ public abstract class AbstractTobagoTestBase extends AbstractJsfTestCase {
     application.addComponent(Tags.selectBooleanToggle.componentType(), UISelectBooleanToggle.class.getName());
     application.addComponent(Tags.selectManyCheckbox.componentType(), UISelectManyCheckbox.class.getName());
     application.addComponent(Tags.selectManyListbox.componentType(), UISelectManyListbox.class.getName());
+    application.addComponent(Tags.selectManyShuttle.componentType(), UISelectManyShuttle.class.getName());
     application.addComponent(Tags.selectOneRadio.componentType(), UISelectOneRadio.class.getName());
     application.addComponent(Tags.selectOneChoice.componentType(), UISelectOneChoice.class.getName());
     application.addComponent(Tags.selectReference.componentType(), UISelectReference.class.getName());
@@ -222,6 +225,8 @@ public abstract class AbstractTobagoTestBase extends AbstractJsfTestCase {
     renderKit.addRenderer(
         UISelectManyListbox.COMPONENT_FAMILY, RendererTypes.SELECT_MANY_LISTBOX, new SelectManyListboxRenderer());
     renderKit.addRenderer(
+        UISelectManyListbox.COMPONENT_FAMILY, RendererTypes.SELECT_MANY_SHUTTLE, new SelectManyShuttleRenderer());
+    renderKit.addRenderer(
         UISelectOneRadio.COMPONENT_FAMILY, RendererTypes.SELECT_ONE_RADIO, new SelectOneRadioRenderer());
     renderKit.addRenderer(
         UISelectOneChoice.COMPONENT_FAMILY, RendererTypes.SELECT_ONE_CHOICE, new SelectOneChoiceRenderer());
diff --git a/tobago-core/src/test/java/org/apache/myfaces/tobago/internal/renderkit/renderer/SelectManyShuttleRendererUnitTest.java b/tobago-core/src/test/java/org/apache/myfaces/tobago/internal/renderkit/renderer/SelectManyShuttleRendererUnitTest.java
new file mode 100644
index 0000000..8df2a0a
--- /dev/null
+++ b/tobago-core/src/test/java/org/apache/myfaces/tobago/internal/renderkit/renderer/SelectManyShuttleRendererUnitTest.java
@@ -0,0 +1,98 @@
+package org.apache.myfaces.tobago.internal.renderkit.renderer;
+
+import org.apache.myfaces.tobago.component.RendererTypes;
+import org.apache.myfaces.tobago.component.Tags;
+import org.apache.myfaces.tobago.component.UISelectItem;
+import org.apache.myfaces.tobago.component.UISelectManyShuttle;
+import org.apache.myfaces.tobago.util.ComponentUtils;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+import javax.faces.application.FacesMessage;
+import java.io.IOException;
+
+public class SelectManyShuttleRendererUnitTest extends RendererTestBase {
+
+  @Test
+  public void simple() throws IOException {
+    final UISelectManyShuttle c = (UISelectManyShuttle) ComponentUtils.createComponent(
+        facesContext, Tags.selectManyShuttle.componentType(), RendererTypes.SelectManyShuttle, "id");
+
+    final UISelectItem i1 = (UISelectItem) ComponentUtils.createComponent(
+        facesContext, Tags.selectItem.componentType(), null, "i1");
+    i1.setItemLabel("Value 1");
+    c.getChildren().add(i1);
+    final UISelectItem i2 = (UISelectItem) ComponentUtils.createComponent(
+        facesContext, Tags.selectItem.componentType(), null, "i2");
+    i2.setItemLabel("Value 2");
+    c.getChildren().add(i2);
+
+    c.encodeAll(facesContext);
+
+    Assertions.assertEquals(loadHtml("renderer/selectManyShuttle/simple.html"), formattedResult());
+  }
+
+  @Test
+  public void label() throws IOException {
+    final UISelectManyShuttle c = (UISelectManyShuttle) ComponentUtils.createComponent(
+        facesContext, Tags.selectManyShuttle.componentType(), RendererTypes.SelectManyShuttle, "id");
+    c.setLabel("label");
+
+    final UISelectItem i1 = (UISelectItem) ComponentUtils.createComponent(
+        facesContext, Tags.selectItem.componentType(), null, "i1");
+    i1.setItemLabel("Value 1");
+    c.getChildren().add(i1);
+    final UISelectItem i2 = (UISelectItem) ComponentUtils.createComponent(
+        facesContext, Tags.selectItem.componentType(), null, "i2");
+    i2.setItemLabel("Value 2");
+    c.getChildren().add(i2);
+
+    c.encodeAll(facesContext);
+
+    Assertions.assertEquals(loadHtml("renderer/selectManyShuttle/label.html"), formattedResult());
+  }
+  //help
+
+
+  @Test
+  public void errorMessage() throws IOException {
+    final UISelectManyShuttle c = (UISelectManyShuttle) ComponentUtils.createComponent(
+        facesContext, Tags.selectManyShuttle.componentType(), RendererTypes.SelectManyShuttle, "id");
+    c.setValid(false);
+    facesContext.addMessage("id",
+        new FacesMessage(FacesMessage.SEVERITY_ERROR, "test", "a test"));
+
+    final UISelectItem i1 = (UISelectItem) ComponentUtils.createComponent(
+        facesContext, Tags.selectItem.componentType(), null, "i1");
+    i1.setItemLabel("Value 1");
+    c.getChildren().add(i1);
+    final UISelectItem i2 = (UISelectItem) ComponentUtils.createComponent(
+        facesContext, Tags.selectItem.componentType(), null, "i2");
+    i2.setItemLabel("Value 2");
+    c.getChildren().add(i2);
+
+    c.encodeAll(facesContext);
+
+    Assertions.assertEquals(loadHtml("renderer/selectManyShuttle/error-message.html"), formattedResult());
+  }
+
+  @Test
+  public void help() throws IOException {
+    final UISelectManyShuttle c = (UISelectManyShuttle) ComponentUtils.createComponent(
+        facesContext, Tags.selectManyShuttle.componentType(), RendererTypes.SelectManyShuttle, "id");
+    c.setHelp("Help!");
+
+    final UISelectItem i1 = (UISelectItem) ComponentUtils.createComponent(
+        facesContext, Tags.selectItem.componentType(), null, "i1");
+    i1.setItemLabel("Value 1");
+    c.getChildren().add(i1);
+    final UISelectItem i2 = (UISelectItem) ComponentUtils.createComponent(
+        facesContext, Tags.selectItem.componentType(), null, "i2");
+    i2.setItemLabel("Value 2");
+    c.getChildren().add(i2);
+
+    c.encodeAll(facesContext);
+
+    Assertions.assertEquals(loadHtml("renderer/selectManyShuttle/help.html"), formattedResult());
+  }
+}
diff --git a/tobago-core/src/test/resources/renderer/selectManyShuttle/error-message.html b/tobago-core/src/test/resources/renderer/selectManyShuttle/error-message.html
new file mode 100644
index 0000000..27621f4
--- /dev/null
+++ b/tobago-core/src/test/resources/renderer/selectManyShuttle/error-message.html
@@ -0,0 +1,45 @@
+<!--
+ * 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.
+-->
+
+<tobago-select-many-shuttle id='id' class='tobago-auto-spacing'>
+  <div class='tobago-messages-container'>
+    <div class='tobago-body' title='a test'>
+      <select id='id::unselected' class='tobago-unselected form-control' multiple='multiple' size='2'>
+        <option value=''>Value 1
+        </option>
+        <option value=''>Value 2
+        </option></select>
+      <div>
+        <div class='btn-group-vertical'>
+          <button type='button' class='btn btn-secondary' id='id::addAll'><i class='bi-chevron-double-right'></i></button>
+          <button type='button' class='btn btn-secondary' id='id::add'><i class='bi-chevron-right'></i></button>
+          <button type='button' class='btn btn-secondary' id='id::remove'><i class='bi-chevron-left'></i></button>
+          <button type='button' class='btn btn-secondary' id='id::removeAll'><i class='bi-chevron-double-left'></i></button>
+        </div>
+      </div>
+      <select id='id::selected' class='tobago-selected border-danger form-control' multiple='multiple' size='2'></select>
+      <select class='d-none' id='id::hidden' name='id' multiple='multiple'>
+        <option value=''>Value 1
+        </option>
+        <option value=''>Value 2
+        </option></select>
+    </div>
+    <tobago-popover data-bs-toggle='popover' title='Error' data-bs-content='a test' data-bs-trigger='focus'>
+      <a tabindex='0' role='button' class='btn btn-danger'><i class='bi-exclamation-lg'></i></a>
+    </tobago-popover>
+  </div>
+</tobago-select-many-shuttle>
\ No newline at end of file
diff --git a/tobago-core/src/test/resources/renderer/selectManyShuttle/help.html b/tobago-core/src/test/resources/renderer/selectManyShuttle/help.html
new file mode 100644
index 0000000..b87cee6
--- /dev/null
+++ b/tobago-core/src/test/resources/renderer/selectManyShuttle/help.html
@@ -0,0 +1,45 @@
+<!--
+ * 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.
+-->
+
+<tobago-select-many-shuttle id='id' class='tobago-auto-spacing'>
+  <div class='tobago-messages-container'>
+    <div class='tobago-body'>
+      <select id='id::unselected' class='tobago-unselected form-control' multiple='multiple' size='2'>
+        <option value=''>Value 1
+        </option>
+        <option value=''>Value 2
+        </option></select>
+      <div>
+        <div class='btn-group-vertical'>
+          <button type='button' class='btn btn-secondary' id='id::addAll'><i class='bi-chevron-double-right'></i></button>
+          <button type='button' class='btn btn-secondary' id='id::add'><i class='bi-chevron-right'></i></button>
+          <button type='button' class='btn btn-secondary' id='id::remove'><i class='bi-chevron-left'></i></button>
+          <button type='button' class='btn btn-secondary' id='id::removeAll'><i class='bi-chevron-double-left'></i></button>
+        </div>
+      </div>
+      <select id='id::selected' class='tobago-selected form-control' multiple='multiple' size='2'></select>
+      <select class='d-none' id='id::hidden' name='id' multiple='multiple'>
+        <option value=''>Value 1
+        </option>
+        <option value=''>Value 2
+        </option></select>
+    </div>
+    <tobago-popover data-bs-toggle='popover' title='Help' data-bs-content='Help!' data-bs-trigger='focus'>
+      <a tabindex='0' role='button' class='btn btn-outline-info'><i class='bi-question-lg'></i></a>
+    </tobago-popover>
+  </div>
+</tobago-select-many-shuttle>
\ No newline at end of file
diff --git a/tobago-core/src/test/resources/renderer/selectManyShuttle/label.html b/tobago-core/src/test/resources/renderer/selectManyShuttle/label.html
new file mode 100644
index 0000000..ecca94e
--- /dev/null
+++ b/tobago-core/src/test/resources/renderer/selectManyShuttle/label.html
@@ -0,0 +1,41 @@
+<!--
+ * 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.
+-->
+
+<tobago-select-many-shuttle id='id' class='tobago-label-container tobago-auto-spacing'>
+  <label for='id::unselected' class='col-form-label'>label</label>
+  <div class='tobago-body'>
+    <select id='id::unselected' class='tobago-unselected form-control' multiple='multiple' size='2'>
+      <option value=''>Value 1
+      </option>
+      <option value=''>Value 2
+      </option></select>
+    <div>
+      <div class='btn-group-vertical'>
+        <button type='button' class='btn btn-secondary' id='id::addAll'><i class='bi-chevron-double-right'></i></button>
+        <button type='button' class='btn btn-secondary' id='id::add'><i class='bi-chevron-right'></i></button>
+        <button type='button' class='btn btn-secondary' id='id::remove'><i class='bi-chevron-left'></i></button>
+        <button type='button' class='btn btn-secondary' id='id::removeAll'><i class='bi-chevron-double-left'></i></button>
+      </div>
+    </div>
+    <select id='id::selected' class='tobago-selected form-control' multiple='multiple' size='2'></select>
+    <select class='d-none' id='id::hidden' name='id' multiple='multiple'>
+      <option value=''>Value 1
+      </option>
+      <option value=''>Value 2
+      </option></select>
+  </div>
+</tobago-select-many-shuttle>
diff --git a/tobago-core/src/test/resources/renderer/selectManyShuttle/simple.html b/tobago-core/src/test/resources/renderer/selectManyShuttle/simple.html
new file mode 100644
index 0000000..b54d2f1
--- /dev/null
+++ b/tobago-core/src/test/resources/renderer/selectManyShuttle/simple.html
@@ -0,0 +1,40 @@
+<!--
+ * 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.
+-->
+
+<tobago-select-many-shuttle id='id' class='tobago-auto-spacing'>
+  <div class='tobago-body'>
+    <select id='id::unselected' class='tobago-unselected form-control' multiple='multiple' size='2'>
+      <option value=''>Value 1
+      </option>
+      <option value=''>Value 2
+      </option></select>
+    <div>
+      <div class='btn-group-vertical'>
+        <button type='button' class='btn btn-secondary' id='id::addAll'><i class='bi-chevron-double-right'></i></button>
+        <button type='button' class='btn btn-secondary' id='id::add'><i class='bi-chevron-right'></i></button>
+        <button type='button' class='btn btn-secondary' id='id::remove'><i class='bi-chevron-left'></i></button>
+        <button type='button' class='btn btn-secondary' id='id::removeAll'><i class='bi-chevron-double-left'></i></button>
+      </div>
+    </div>
+    <select id='id::selected' class='tobago-selected form-control' multiple='multiple' size='2'></select>
+    <select class='d-none' id='id::hidden' name='id' multiple='multiple'>
+      <option value=''>Value 1
+      </option>
+      <option value=''>Value 2
+      </option></select>
+  </div>
+</tobago-select-many-shuttle>
\ No newline at end of file
diff --git a/tobago-theme/src/main/scss/_tobago.scss b/tobago-theme/src/main/scss/_tobago.scss
index 0e4cbbc..847fc9d 100644
--- a/tobago-theme/src/main/scss/_tobago.scss
+++ b/tobago-theme/src/main/scss/_tobago.scss
@@ -392,14 +392,7 @@ tobago-flex-layout {
 
   > {
     .form-control, .form-control-plaintext,
-    .input-group, .tobago-messages-container,
-    .tobago-selectManyShuttle {
-      flex: 1 0 0px;
-    }
-  }
-
-  > .tobago-messages-container {
-    .tobago-selectManyShuttle {
+    .input-group, .tobago-messages-container {
       flex: 1 0 0px;
     }
   }
@@ -449,7 +442,7 @@ tobago-grid-layout {
   column-gap: $spacer;
 
   .tobago-label-container > {
-    textarea, .tobago-selectManyListbox, .tobago-selectManyShuttle {
+    textarea, .tobago-selectManyListbox {
       height: 100%;
     }
   }
@@ -470,12 +463,6 @@ tobago-header {
   }
 }
 
-/* hidden ----------------------------------------------------------- */
-
-.tobago-hidden:disabled {
-  display: none;
-}
-
 /* image ----------------------------------------------------------- */
 
 tobago-image.disabled {
@@ -1149,45 +1136,28 @@ tobago-select-many-listbox {
 /* selectManyShuttle ----------------------------------------------------------- */
 tobago-select-many-shuttle {
   display: block;
-}
-
-.tobago-selectManyShuttle-add,
-.tobago-selectManyShuttle-addAll,
-.tobago-selectManyShuttle-option,
-.tobago-selectManyShuttle-remove,
-.tobago-selectManyShuttle-removeAll,
-.tobago-selectManyShuttle-selectedLabel,
-.tobago-selectManyShuttle-unselectedLabel {
-}
-
-.tobago-selectManyShuttle {
-  display: flex;
 
-  .tobago-selectManyShuttle-unselected,
-  .tobago-selectManyShuttle-selected {
-    flex: 1 0 0px;
-  }
-}
-
-.tobago-selectManyShuttle-hidden {
-  display: none;
-}
+  .tobago-body {
+    display: flex;
+    height: 100%; //size should be the same as the outer custom tag; important for horizontal grid layout
 
-.tobago-selectManyShuttle-toolBar {
-  display: flex;
-  flex-direction: column;
-  padding: 0 0.5rem;
+    .btn-group-vertical {
+      padding-left: $list-inline-padding;
+      padding-right: $list-inline-padding;
+    }
 
-  > button {
-    display: block;
-  }
+    .tobago-unselected {
+    }
 
-  > div {
-    flex: 1 0 0px;
+    .tobago-selected {
+      @include messageShadow()
+    }
   }
 
-  > * { /* XXX this fixes the margin left from .btn:nth-child(n+2), but is ugly */
-    margin-left: 0 !important;
+  &.tobago-label-container, .tobago-messages-container {
+    > .tobago-body {
+      flex: 1 0 0px;
+    }
   }
 }
 
diff --git a/tobago-theme/tobago-theme-standard/src/main/ts/tobago-select-many-shuttle.ts b/tobago-theme/tobago-theme-standard/src/main/ts/tobago-select-many-shuttle.ts
index 40a3443..1be8090 100644
--- a/tobago-theme/tobago-theme-standard/src/main/ts/tobago-select-many-shuttle.ts
+++ b/tobago-theme/tobago-theme-standard/src/main/ts/tobago-select-many-shuttle.ts
@@ -89,31 +89,31 @@ class SelectManyShuttle extends HTMLElement {
   }
 
   get unselectedSelect(): HTMLSelectElement {
-    return this.querySelector(".tobago-selectManyShuttle-unselected");
+    return this.querySelector(".tobago-unselected");
   }
 
   get selectedSelect(): HTMLSelectElement {
-    return this.querySelector(".tobago-selectManyShuttle-selected");
+    return this.querySelector(".tobago-selected");
   }
 
   get hiddenSelect(): HTMLSelectElement {
-    return this.querySelector(".tobago-selectManyShuttle-hidden");
+    return this.querySelector("select.d-none");
   }
 
   get addAllButton(): HTMLButtonElement {
-    return this.querySelector(".tobago-selectManyShuttle-addAll");
+    return this.querySelector(".btn-group-vertical button:nth-child(1)");
   }
 
   get addButton(): HTMLButtonElement {
-    return this.querySelector(".tobago-selectManyShuttle-add");
+    return this.querySelector(".btn-group-vertical button:nth-child(2)");
   }
 
   get removeButton(): HTMLButtonElement {
-    return this.querySelector(".tobago-selectManyShuttle-remove");
+    return this.querySelector(".btn-group-vertical button:nth-child(3)");
   }
 
   get removeAllButton(): HTMLButtonElement {
-    return this.querySelector(".tobago-selectManyShuttle-removeAll");
+    return this.querySelector(".btn-group-vertical button:nth-child(4)");
   }
 }