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 2022/12/02 16:06:06 UTC

[myfaces-tobago] 01/02: feat: selectManyList component

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 eb2cca850e1f692304ff7173bfd1f878b9b792a5
Author: Henning Noeth <hn...@apache.org>
AuthorDate: Wed Oct 26 22:01:10 2022 +0200

    feat: selectManyList component
    
    * add new tc:selectManyList component
    
    Issue: TOBAGO-2159
---
 .../myfaces/tobago/component/RendererTypes.java    |   2 +
 .../myfaces/tobago/component/SupportsFilter.java   |  25 ++
 .../org/apache/myfaces/tobago/component/Tags.java  |   2 +
 .../component/AbstractUISelectManyList.java        |  84 ++++
 .../renderkit/renderer/SelectManyListRenderer.java | 209 ++++++++++
 .../component/SelectManyListTagDeclaration.java    |  94 +++++
 .../internal/taglib/declaration/HasFilter.java     |  29 ++
 .../tobago/renderkit/css/BootstrapClass.java       |   3 +
 .../myfaces/tobago/renderkit/css/TobagoClass.java  |   5 +
 .../tobago/renderkit/html/HtmlElements.java        |   1 +
 .../example/demo/SelectManyListController.java     | 138 +++++++
 .../apache/myfaces/tobago/example/demo/names.txt   |  75 ++++
 .../80-selectManyList/SelectManyList.xhtml         |  87 +++++
 tobago-theme/jest.config.js                        |   5 +-
 tobago-theme/src/main/scss/_tobago.scss            | 161 ++++++++
 .../src/main/resources/META-INF/tobago-config.xml  |   3 +
 .../src/main/ts/tobago-all.ts                      |   1 +
 .../src/main/ts/tobago-filter-registry.test.ts     | 124 ++++++
 .../src/main/ts/tobago-filter-registry.ts          | 102 +++++
 .../src/main/ts/tobago-select-many-list.ts         | 421 +++++++++++++++++++++
 20 files changed, 1570 insertions(+), 1 deletion(-)

diff --git a/tobago-core/src/main/java/org/apache/myfaces/tobago/component/RendererTypes.java b/tobago-core/src/main/java/org/apache/myfaces/tobago/component/RendererTypes.java
index 27e90ae5fe..d77b7b1c31 100644
--- a/tobago-core/src/main/java/org/apache/myfaces/tobago/component/RendererTypes.java
+++ b/tobago-core/src/main/java/org/apache/myfaces/tobago/component/RendererTypes.java
@@ -68,6 +68,7 @@ public enum RendererTypes {
   SelectBooleanCheckbox,
   SelectBooleanToggle,
   SelectManyCheckbox,
+  SelectManyList,
   SelectManyListbox,
   SelectManyShuttle,
   SelectOneChoice,
@@ -141,6 +142,7 @@ public enum RendererTypes {
   public static final String SELECT_BOOLEAN_CHECKBOX = "SelectBooleanCheckbox";
   public static final String SELECT_BOOLEAN_TOGGLE = "SelectBooleanToggle";
   public static final String SELECT_MANY_CHECKBOX = "SelectManyCheckbox";
+  public static final String SELECT_MANY_LIST = "SelectManyList";
   public static final String SELECT_MANY_LISTBOX = "SelectManyListbox";
   public static final String SELECT_MANY_SHUTTLE = "SelectManyShuttle";
   public static final String SELECT_ONE_CHOICE = "SelectOneChoice";
diff --git a/tobago-core/src/main/java/org/apache/myfaces/tobago/component/SupportsFilter.java b/tobago-core/src/main/java/org/apache/myfaces/tobago/component/SupportsFilter.java
new file mode 100644
index 0000000000..a61bdbf953
--- /dev/null
+++ b/tobago-core/src/main/java/org/apache/myfaces/tobago/component/SupportsFilter.java
@@ -0,0 +1,25 @@
+/*
+ * 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.component;
+
+public interface SupportsFilter {
+
+  String getFilter();
+}
diff --git a/tobago-core/src/main/java/org/apache/myfaces/tobago/component/Tags.java b/tobago-core/src/main/java/org/apache/myfaces/tobago/component/Tags.java
index 6620e764e2..9b6050a801 100644
--- a/tobago-core/src/main/java/org/apache/myfaces/tobago/component/Tags.java
+++ b/tobago-core/src/main/java/org/apache/myfaces/tobago/component/Tags.java
@@ -75,6 +75,7 @@ public enum Tags {
   selectItem,
   selectItems,
   selectManyCheckbox,
+  selectManyList,
   selectManyListbox,
   selectManyShuttle,
   selectOneChoice,
@@ -147,6 +148,7 @@ public enum Tags {
   public static final String SELECT_ITEM = "selectItem";
   public static final String SELECT_ITEMS = "selectItems";
   public static final String SELECT_MANY_CHECKBOX = "selectManyCheckbox";
+  public static final String SELECT_MANY_LIST = "selectManyList";
   public static final String SELECT_MANY_LISTBOX = "selectManyListbox";
   public static final String SELECT_MANY_SHUTTLE = "selectManyShuttle";
   public static final String SELECT_ONE_CHOICE = "selectOneChoice";
diff --git a/tobago-core/src/main/java/org/apache/myfaces/tobago/internal/component/AbstractUISelectManyList.java b/tobago-core/src/main/java/org/apache/myfaces/tobago/internal/component/AbstractUISelectManyList.java
new file mode 100644
index 0000000000..952c1ddb9f
--- /dev/null
+++ b/tobago-core/src/main/java/org/apache/myfaces/tobago/internal/component/AbstractUISelectManyList.java
@@ -0,0 +1,84 @@
+/*
+ * 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.internal.component;
+
+import org.apache.myfaces.tobago.component.SupportFieldId;
+import org.apache.myfaces.tobago.component.SupportsAutoSpacing;
+import org.apache.myfaces.tobago.component.SupportsFilter;
+import org.apache.myfaces.tobago.component.SupportsHelp;
+import org.apache.myfaces.tobago.component.SupportsLabelLayout;
+import org.apache.myfaces.tobago.component.Visual;
+import org.apache.myfaces.tobago.internal.taglib.component.SelectManyListTagDeclaration;
+import org.apache.myfaces.tobago.util.ComponentUtils;
+
+import javax.faces.component.behavior.ClientBehaviorHolder;
+import javax.faces.context.FacesContext;
+import java.util.Collection;
+
+/**
+ * {@link SelectManyListTagDeclaration}
+ */
+public abstract class AbstractUISelectManyList extends AbstractUISelectManyBase
+  implements SupportsAutoSpacing, Visual, SupportsLabelLayout, ClientBehaviorHolder, SupportsHelp, SupportFieldId,
+  SupportsFilter {
+
+  private transient boolean nextToRenderIsLabel;
+
+  @Override
+  public Object[] getSelectedValues() {
+    final Object value = getValue();
+    if (value instanceof Collection) {
+      return ((Collection) value).toArray();
+    } else {
+      return (Object[]) value;
+    }
+  }
+
+  @Override
+  public String getFieldId(final FacesContext facesContext) {
+    return getClientId(facesContext) + ComponentUtils.SUB_SEPARATOR + "field";
+  }
+
+  public abstract Integer getTabIndex();
+
+  public abstract boolean isDisabled();
+
+  public abstract boolean isInline();
+
+  public boolean isError() {
+    final FacesContext facesContext = FacesContext.getCurrentInstance();
+    return !isValid()
+      || !facesContext.getMessageList(getClientId(facesContext)).isEmpty();
+  }
+
+  public abstract boolean isFocus();
+
+  public abstract String getFilter();
+
+  @Override
+  public boolean isNextToRenderIsLabel() {
+    return nextToRenderIsLabel;
+  }
+
+  @Override
+  public void setNextToRenderIsLabel(final boolean nextToRenderIsLabel) {
+    this.nextToRenderIsLabel = nextToRenderIsLabel;
+  }
+}
diff --git a/tobago-core/src/main/java/org/apache/myfaces/tobago/internal/renderkit/renderer/SelectManyListRenderer.java b/tobago-core/src/main/java/org/apache/myfaces/tobago/internal/renderkit/renderer/SelectManyListRenderer.java
new file mode 100644
index 0000000000..d6ebee277d
--- /dev/null
+++ b/tobago-core/src/main/java/org/apache/myfaces/tobago/internal/renderkit/renderer/SelectManyListRenderer.java
@@ -0,0 +1,209 @@
+/*
+ * 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.internal.renderkit.renderer;
+
+import org.apache.myfaces.tobago.context.Markup;
+import org.apache.myfaces.tobago.internal.component.AbstractUISelectManyList;
+import org.apache.myfaces.tobago.internal.util.ArrayUtils;
+import org.apache.myfaces.tobago.internal.util.HtmlRendererUtils;
+import org.apache.myfaces.tobago.internal.util.SelectItemUtils;
+import org.apache.myfaces.tobago.renderkit.css.BootstrapClass;
+import org.apache.myfaces.tobago.renderkit.css.CssItem;
+import org.apache.myfaces.tobago.renderkit.css.TobagoClass;
+import org.apache.myfaces.tobago.renderkit.html.Arias;
+import org.apache.myfaces.tobago.renderkit.html.CustomAttributes;
+import org.apache.myfaces.tobago.renderkit.html.DataAttributes;
+import org.apache.myfaces.tobago.renderkit.html.HtmlAttributes;
+import org.apache.myfaces.tobago.renderkit.html.HtmlElements;
+import org.apache.myfaces.tobago.renderkit.html.HtmlInputTypes;
+import org.apache.myfaces.tobago.util.ComponentUtils;
+import org.apache.myfaces.tobago.webapp.TobagoResponseWriter;
+
+import javax.faces.context.FacesContext;
+import javax.faces.model.SelectItem;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+
+public class SelectManyListRenderer<T extends AbstractUISelectManyList> extends SelectManyRendererBase<T> {
+  @Override
+  public HtmlElements getComponentTag() {
+    return HtmlElements.TOBAGO_SELECT_MANY_LIST;
+  }
+
+  @Override
+  protected CssItem[] getComponentCss(final FacesContext facesContext, final T component) {
+    final List<SelectItem> items = SelectItemUtils.getItemList(facesContext, component);
+    final boolean disabled = !items.iterator().hasNext() || component.isDisabled() || component.isReadonly();
+
+    List<CssItem> cssItems = new ArrayList<>();
+    if (disabled) {
+      cssItems.add(TobagoClass.DISABLED);
+    }
+    return cssItems.toArray(new CssItem[0]);
+  }
+
+  @Override
+  protected String getFieldId(FacesContext facesContext, T component) {
+    return component.getFieldId(facesContext);
+  }
+
+  @Override
+  protected void encodeBeginField(FacesContext facesContext, T component) throws IOException {
+    final TobagoResponseWriter writer = getResponseWriter(facesContext);
+
+    final String clientId = component.getClientId(facesContext);
+    final String fieldId = component.getFieldId(facesContext);
+    final String filterId = clientId + ComponentUtils.SUB_SEPARATOR + "filter";
+    final String selectedId = clientId + ComponentUtils.SUB_SEPARATOR + "selected";
+    final List<SelectItem> items = SelectItemUtils.getItemList(facesContext, component);
+    final boolean disabled = !items.iterator().hasNext() || component.isDisabled() || component.isReadonly();
+    final String filter = component.getFilter();
+    final boolean inline = component.isInline();
+    final Markup markup = component.getMarkup();
+    final String title = HtmlRendererUtils.getTitleFromTipAndMessages(facesContext, component);
+    final Integer tabIndex = component.getTabIndex();
+
+    encodeHiddenSelect(facesContext, component, items, clientId, selectedId, disabled);
+
+    writer.startElement(HtmlElements.DIV);
+    writer.writeClassAttribute(
+        inline ? BootstrapClass.LIST_GROUP : BootstrapClass.DROPDOWN,
+        inline ? BootstrapClass.borderColor(ComponentUtils.getMaximumSeverity(component)) : null);
+
+    encodeSelectField(facesContext, component, clientId, fieldId, filterId, filter, disabled, inline, title, tabIndex);
+    encodeOptions(facesContext, component, items, clientId, inline, disabled);
+
+    writer.endElement(HtmlElements.DIV);
+  }
+
+  @Override
+  protected void writeAdditionalAttributes(FacesContext facesContext, TobagoResponseWriter writer, T input)
+      throws IOException {
+    super.writeAdditionalAttributes(facesContext, writer, input);
+    writer.writeAttribute(CustomAttributes.FILTER, input.getFilter(), true);
+  }
+
+  private void encodeHiddenSelect(final FacesContext facesContext, final T component, final List<SelectItem> items,
+      final String clientId, final String selectedId, final boolean disabled) throws IOException {
+    final TobagoResponseWriter writer = getResponseWriter(facesContext);
+
+    writer.startElement(HtmlElements.SELECT);
+    writer.writeIdAttribute(selectedId);
+    writer.writeNameAttribute(clientId);
+    writer.writeAttribute(HtmlAttributes.DISABLED, disabled);
+    writer.writeAttribute(HtmlAttributes.REQUIRED, component.isRequired());
+    writer.writeClassAttribute(BootstrapClass.D_NONE);
+    writer.writeAttribute(HtmlAttributes.MULTIPLE, true);
+
+    final Object[] values = component.getSelectedValues();
+    final String[] submittedValues = getSubmittedValues(component);
+    renderSelectItems(component, null, items, values, submittedValues, writer, facesContext);
+    writer.endElement(HtmlElements.SELECT);
+  }
+
+  private void encodeSelectField(final FacesContext facesContext, final T component,
+      final String clientId, final String fieldId, final String filterId, final String filter, final boolean disabled,
+      final boolean inline, final String title, final Integer tabIndex) throws IOException {
+    final TobagoResponseWriter writer = getResponseWriter(facesContext);
+
+    writer.startElement(HtmlElements.DIV);
+    writer.writeIdAttribute(fieldId);
+    writer.writeNameAttribute(clientId);
+    HtmlRendererUtils.writeDataAttributes(facesContext, writer, component);
+    writer.writeClassAttribute(
+        inline ? BootstrapClass.FORM_CONTROL : BootstrapClass.FORM_SELECT,
+        TobagoClass.SELECT__FIELD,
+        inline ? BootstrapClass.LIST_GROUP_ITEM : BootstrapClass.DROPDOWN_TOGGLE,
+        inline ? null : BootstrapClass.borderColor(ComponentUtils.getMaximumSeverity(component)),
+        component.getCustomClass());
+    writer.writeAttribute(HtmlAttributes.TITLE, title, true);
+    writer.writeAttribute(Arias.EXPANDED, Boolean.FALSE.toString(), false);
+    writer.writeAttribute(HtmlAttributes.DISABLED, disabled);
+
+    writer.startElement(HtmlElements.INPUT);
+    writer.writeAttribute(HtmlAttributes.TYPE, HtmlInputTypes.TEXT);
+    writer.writeIdAttribute(filterId);
+    writer.writeClassAttribute(TobagoClass.FILTER, BootstrapClass.FORM_CONTROL);
+    writer.writeAttribute(HtmlAttributes.AUTOCOMPLETE, "off", false);
+    writer.writeAttribute(HtmlAttributes.READONLY, filter == null || filter.isEmpty());
+    writer.writeAttribute(HtmlAttributes.DISABLED, disabled);
+    writer.writeAttribute(HtmlAttributes.TABINDEX, tabIndex);
+    renderFocus(clientId, component.isFocus(), component.isError(), facesContext, writer);
+
+    writer.endElement(HtmlElements.INPUT);
+
+    writer.endElement(HtmlElements.DIV);
+  }
+
+  private void encodeOptions(final FacesContext facesContext, final T component, final List<SelectItem> items,
+      final String clientId, final boolean inline, final boolean disabled) throws IOException {
+    final TobagoResponseWriter writer = getResponseWriter(facesContext);
+
+    writer.startElement(HtmlElements.DIV);
+    writer.writeClassAttribute(
+        TobagoClass.OPTIONS,
+        inline ? BootstrapClass.LIST_GROUP_ITEM : BootstrapClass.DROPDOWN_MENU);
+    writer.writeNameAttribute(clientId);
+
+    writer.startElement(HtmlElements.TABLE);
+    writer.writeClassAttribute(BootstrapClass.TABLE, BootstrapClass.TABLE_HOVER, BootstrapClass.TABLE_SM);
+    writer.startElement(HtmlElements.TBODY);
+
+    final Object[] values = component.getSelectedValues();
+    final String[] submittedValues = getSubmittedValues(component);
+    for (SelectItem item : items) {
+      Object itemValue = item.getValue();
+      // when using selectItem tag with a literal value: use the converted value
+      if (itemValue instanceof String && values != null && values.length > 0 && !(values[0] instanceof String)) {
+        itemValue = ComponentUtils.getConvertedValue(facesContext, component, (String) itemValue);
+      }
+      final String formattedValue = getFormattedValue(facesContext, (T) component, itemValue);
+      final boolean contains;
+      if (submittedValues == null) {
+        contains = ArrayUtils.contains(values, itemValue);
+      } else {
+        contains = ArrayUtils.contains(submittedValues, formattedValue);
+      }
+      writer.startElement(HtmlElements.TR);
+      writer.writeAttribute(DataAttributes.VALUE, formattedValue, true);
+      writer.writeClassAttribute(
+          contains ? BootstrapClass.TABLE_PRIMARY : null,
+          disabled || item.isDisabled() ? TobagoClass.DISABLED : null);
+
+      writer.startElement(HtmlElements.TD);
+      writer.writeAttribute(HtmlAttributes.VALUE, formattedValue, true);
+      writer.writeText(item.getLabel());
+      writer.endElement(HtmlElements.TD);
+      writer.endElement(HtmlElements.TR);
+    }
+
+    writer.endElement(HtmlElements.TBODY);
+    writer.endElement(HtmlElements.TABLE);
+    writer.endElement(HtmlElements.DIV);
+  }
+
+  @Override
+  protected void encodeEndField(FacesContext facesContext, T component) throws IOException {
+    final TobagoResponseWriter writer = getResponseWriter(facesContext);
+
+    encodeBehavior(writer, facesContext, component);
+  }
+}
diff --git a/tobago-core/src/main/java/org/apache/myfaces/tobago/internal/taglib/component/SelectManyListTagDeclaration.java b/tobago-core/src/main/java/org/apache/myfaces/tobago/internal/taglib/component/SelectManyListTagDeclaration.java
new file mode 100644
index 0000000000..e7572d504b
--- /dev/null
+++ b/tobago-core/src/main/java/org/apache/myfaces/tobago/internal/taglib/component/SelectManyListTagDeclaration.java
@@ -0,0 +1,94 @@
+/*
+ * 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.internal.taglib.component;
+
+import org.apache.myfaces.tobago.apt.annotation.Behavior;
+import org.apache.myfaces.tobago.apt.annotation.Preliminary;
+import org.apache.myfaces.tobago.apt.annotation.Tag;
+import org.apache.myfaces.tobago.apt.annotation.TagAttribute;
+import org.apache.myfaces.tobago.apt.annotation.UIComponentTag;
+import org.apache.myfaces.tobago.apt.annotation.UIComponentTagAttribute;
+import org.apache.myfaces.tobago.component.ClientBehaviors;
+import org.apache.myfaces.tobago.component.RendererTypes;
+import org.apache.myfaces.tobago.internal.taglib.declaration.HasAutoSpacing;
+import org.apache.myfaces.tobago.internal.taglib.declaration.HasBinding;
+import org.apache.myfaces.tobago.internal.taglib.declaration.HasConverter;
+import org.apache.myfaces.tobago.internal.taglib.declaration.HasConverterMessage;
+import org.apache.myfaces.tobago.internal.taglib.declaration.HasFilter;
+import org.apache.myfaces.tobago.internal.taglib.declaration.HasHelp;
+import org.apache.myfaces.tobago.internal.taglib.declaration.HasId;
+import org.apache.myfaces.tobago.internal.taglib.declaration.HasLabel;
+import org.apache.myfaces.tobago.internal.taglib.declaration.HasLabelLayout;
+import org.apache.myfaces.tobago.internal.taglib.declaration.HasRequiredMessageForSelect;
+import org.apache.myfaces.tobago.internal.taglib.declaration.HasTabIndex;
+import org.apache.myfaces.tobago.internal.taglib.declaration.HasTip;
+import org.apache.myfaces.tobago.internal.taglib.declaration.HasValidator;
+import org.apache.myfaces.tobago.internal.taglib.declaration.HasValidatorMessage;
+import org.apache.myfaces.tobago.internal.taglib.declaration.HasValueChangeListener;
+import org.apache.myfaces.tobago.internal.taglib.declaration.IsDisabled;
+import org.apache.myfaces.tobago.internal.taglib.declaration.IsFocus;
+import org.apache.myfaces.tobago.internal.taglib.declaration.IsInline;
+import org.apache.myfaces.tobago.internal.taglib.declaration.IsReadonly;
+import org.apache.myfaces.tobago.internal.taglib.declaration.IsRendered;
+import org.apache.myfaces.tobago.internal.taglib.declaration.IsRequiredForSelect;
+import org.apache.myfaces.tobago.internal.taglib.declaration.IsVisual;
+
+import javax.faces.component.UISelectMany;
+
+/**
+ * Render a multi selection option listbox.
+ */
+@Preliminary
+@Tag(name = "selectManyList")
+@UIComponentTag(
+  uiComponent = "org.apache.myfaces.tobago.component.UISelectManyList",
+  uiComponentFacesClass = "javax.faces.component.UISelectMany",
+  componentFamily = UISelectMany.COMPONENT_FAMILY,
+  rendererType = RendererTypes.SELECT_MANY_LIST,
+  allowedChildComponenents = {"javax.faces.SelectItem", "javax.faces.SelectItems"},
+  behaviors = {
+    @Behavior(
+      name = ClientBehaviors.CHANGE,
+      isDefault = true),
+    @Behavior(
+      name = ClientBehaviors.INPUT),
+    @Behavior(
+      name = ClientBehaviors.CLICK),
+    @Behavior(
+      name = ClientBehaviors.DBLCLICK),
+    @Behavior(
+      name = ClientBehaviors.FOCUS),
+    @Behavior(
+      name = ClientBehaviors.BLUR)
+  })
+
+public interface SelectManyListTagDeclaration
+  extends HasId, IsDisabled, IsRendered, HasBinding, HasTip, HasHelp,
+  IsReadonly, HasConverter, IsRequiredForSelect, HasLabel, HasValidator, HasValueChangeListener, HasLabelLayout,
+  HasValidatorMessage, HasConverterMessage, HasRequiredMessageForSelect, HasTabIndex, IsFocus, IsVisual,
+  HasAutoSpacing, HasFilter, IsInline {
+
+  /**
+   * The value of the multi select.
+   */
+  @TagAttribute
+  @UIComponentTagAttribute(type = {"java.lang.Object[]", "java.util.List"})
+  void setValue(String value);
+}
diff --git a/tobago-core/src/main/java/org/apache/myfaces/tobago/internal/taglib/declaration/HasFilter.java b/tobago-core/src/main/java/org/apache/myfaces/tobago/internal/taglib/declaration/HasFilter.java
new file mode 100644
index 0000000000..b11db54a21
--- /dev/null
+++ b/tobago-core/src/main/java/org/apache/myfaces/tobago/internal/taglib/declaration/HasFilter.java
@@ -0,0 +1,29 @@
+/*
+ * 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.internal.taglib.declaration;
+
+import org.apache.myfaces.tobago.apt.annotation.TagAttribute;
+import org.apache.myfaces.tobago.apt.annotation.UIComponentTagAttribute;
+
+public interface HasFilter {
+  @TagAttribute
+  @UIComponentTagAttribute
+  void setFilter(String filter);
+}
diff --git a/tobago-core/src/main/java/org/apache/myfaces/tobago/renderkit/css/BootstrapClass.java b/tobago-core/src/main/java/org/apache/myfaces/tobago/renderkit/css/BootstrapClass.java
index 538a92ae08..f41e2921db 100644
--- a/tobago-core/src/main/java/org/apache/myfaces/tobago/renderkit/css/BootstrapClass.java
+++ b/tobago-core/src/main/java/org/apache/myfaces/tobago/renderkit/css/BootstrapClass.java
@@ -410,6 +410,8 @@ public enum BootstrapClass implements CssItem {
   JUSTIFY_CONTENT_CENTER("justify-content-center"),
   JUSTIFY_CONTENT_END("justify-content-end"),
   JUSTIFY_CONTENT_START("justify-content-start"),
+  LIST_GROUP("list-group"),
+  LIST_GROUP_ITEM("list-group-item"),
   MB_0("mb-0"),
   MB_1("mb-1"),
   MB_2("mb-2"),
@@ -614,6 +616,7 @@ public enum BootstrapClass implements CssItem {
   TABLE_DARK("table-dark"),
   TABLE_HOVER("table-hover"),
   TABLE_INFO("table-info"),
+  TABLE_PRIMARY("table-primary"),
   TABLE_SM("table-sm"),
   TABLE_STRIPED("table-striped"),
   TOOLTIP_ARROW("tooltip-arrow"),
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 6e2212b726..91af83d7ed 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
@@ -49,12 +49,15 @@ public enum TobagoClass implements CssItem {
 //  DATE__PICKER("tobago-date-picker"),
   DELETED("tobago-deleted"),
   DESCENDING("tobago-descending"),
+  DISABLED("tobago-disabled"),
   DISPLAY__INLINE__BLOCK("tobago-display-inline-block"),
   DROPDOWN__SUBMENU("tobago-dropdown-submenu"),
   EXPANDED("tobago-expanded"),
   //  FILE("tobago-file"),
 //  FIGURE("tobago-figure"),
+  FOCUS("tobago-focus"),
   FOLDER("tobago-folder"),
+  FILTER("tobago-filter"),
   HEADER("tobago-header"),
   //  IMAGE("tobago-image"),
   // tbd: can be removed?
@@ -69,6 +72,7 @@ public enum TobagoClass implements CssItem {
   NOW("tobago-now"),
   NUMBER("tobago-number"),
   OBJECT("tobago-object"),
+  OPTIONS("tobago-options"),
   OUT("tobago-out"),
   //  PAGE("tobago-page"),
   PAGE__MENU_STORE("tobago-page-menuStore"),
@@ -80,6 +84,7 @@ public enum TobagoClass implements CssItem {
   REQUIRED("tobago-required"),
   RESIZE("tobago-resize"),
   SECTION__CONTENT("tobago-section-content"),
+  SELECT__FIELD("tobago-select-field"),
   SELECT_MANY_LISTBOX__OPTION("tobago-selectManyListbox-option"),
   //  SELECT_MANY_SHUTTLE("tobago-selectManyShuttle"),
 //  SELECT_MANY_SHUTTLE__ADD("tobago-selectManyShuttle-add"),
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 5e98ec05ea..727dc64f3d 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
@@ -172,6 +172,7 @@ 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_LIST("tobago-select-many-list"),
   TOBAGO_SELECT_MANY_LISTBOX("tobago-select-many-listbox"),
   TOBAGO_SELECT_MANY_SHUTTLE("tobago-select-many-shuttle"),
   TOBAGO_SELECT_ONE_CHOICE("tobago-select-one-choice"),
diff --git a/tobago-example/tobago-example-demo/src/main/java/org/apache/myfaces/tobago/example/demo/SelectManyListController.java b/tobago-example/tobago-example-demo/src/main/java/org/apache/myfaces/tobago/example/demo/SelectManyListController.java
new file mode 100644
index 0000000000..023b3c826e
--- /dev/null
+++ b/tobago-example/tobago-example-demo/src/main/java/org/apache/myfaces/tobago/example/demo/SelectManyListController.java
@@ -0,0 +1,138 @@
+/*
+ * 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;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import javax.annotation.PostConstruct;
+import javax.enterprise.context.SessionScoped;
+import javax.faces.application.FacesMessage;
+import javax.faces.context.FacesContext;
+import javax.inject.Inject;
+import javax.inject.Named;
+import java.io.BufferedReader;
+import java.io.InputStreamReader;
+import java.io.Serializable;
+import java.lang.invoke.MethodHandles;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.List;
+
+@SessionScoped
+@Named
+public class SelectManyListController implements Serializable {
+
+  private static final Logger LOG = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
+
+  @Inject
+  private AstroData astroData;
+
+  private List<SolarObject> planets;
+  private SolarObject[] selected1 = new SolarObject[0];
+  private SolarObject[] selected2 = new SolarObject[0];
+  private SolarObject[] selected3 = new SolarObject[0];
+  private SolarObject[] selected4 = new SolarObject[0];
+  private List<String> names;
+  private String[] selected5 = new String[0];
+
+  private String filterType;
+
+  @PostConstruct
+  public void init() {
+    planets = astroData.getSatellites("Sun");
+
+    names = new ArrayList<>();
+    try (BufferedReader reader = new BufferedReader(new InputStreamReader(
+        Thread.currentThread().getContextClassLoader().getResourceAsStream(
+            "org/apache/myfaces/tobago/example/demo/names.txt"), StandardCharsets.UTF_8))) {
+      String line;
+      while ((line = reader.readLine()) != null) {
+        line = line.trim();
+        if (!line.startsWith("#") && line.length() > 0) {
+          names.add(line);
+        }
+      }
+    } catch (Exception e) {
+      LOG.error("Can't load names", e);
+    }
+  }
+
+  public List<SolarObject> getPlanets() {
+    return planets;
+  }
+
+  public SolarObject[] getSelected1() {
+    return selected1;
+  }
+
+  public void setSelected1(SolarObject[] selected1) {
+    this.selected1 = selected1;
+  }
+
+  public SolarObject[] getSelected2() {
+    return selected2;
+  }
+
+  public void setSelected2(SolarObject[] selected2) {
+    this.selected2 = selected2;
+  }
+
+  public SolarObject[] getSelected3() {
+    return selected3;
+  }
+
+  public void setSelected3(SolarObject[] selected3) {
+    this.selected3 = selected3;
+  }
+
+  public SolarObject[] getSelected4() {
+    return selected4;
+  }
+
+  public void setSelected4(SolarObject[] selected4) {
+    this.selected4 = selected4;
+  }
+
+  public List<String> getNames() {
+    if (names.size() < 1) {
+      FacesContext.getCurrentInstance().addMessage(null,
+          new FacesMessage(FacesMessage.SEVERITY_ERROR, "Names not loaded!", null));
+      return new ArrayList<>();
+    }
+    return names;
+  }
+
+  public String[] getSelected5() {
+    return selected5;
+  }
+
+  public void setSelected5(String[] selected5) {
+    this.selected5 = selected5;
+  }
+
+  public String getFilterType() {
+    return filterType;
+  }
+
+  public void setFilterType(String filterType) {
+    this.filterType = filterType;
+  }
+}
diff --git a/tobago-example/tobago-example-demo/src/main/resources/org/apache/myfaces/tobago/example/demo/names.txt b/tobago-example/tobago-example-demo/src/main/resources/org/apache/myfaces/tobago/example/demo/names.txt
new file mode 100644
index 0000000000..cff719831c
--- /dev/null
+++ b/tobago-example/tobago-example-demo/src/main/resources/org/apache/myfaces/tobago/example/demo/names.txt
@@ -0,0 +1,75 @@
+# 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.
+
+Adrián
+Agnès
+Amélie
+André
+Asbjörn
+Asbjørn
+Behlül
+Björn
+Börje
+Çağlar
+Celâl
+Chloé
+Chloë
+François
+Gökay
+Gökhan
+Göran
+Gösta
+Gustav
+Güvençe
+Héloise
+Inès
+János
+Jokūbas
+Jöran
+KŠthe
+Léa
+Léo
+Maél
+Maël
+Maël
+Mathéo
+Mátyás
+Miraç
+Mjølnir
+Mónica
+Noël
+Nurdoğan
+Örjan
+Özer
+Özgür
+Öztürk
+Raphaël
+Renée
+Ruairí
+Rüzgar
+Rüştü
+Seán
+Siân
+Sölve
+Sönke
+Sørina
+Timéo
+Tunçay
+Ümit
+Uğur
+Yağmur
+Yiğitcan
+Zoë
+Zülfikar
diff --git a/tobago-example/tobago-example-demo/src/main/webapp/content/030-select/80-selectManyList/SelectManyList.xhtml b/tobago-example/tobago-example-demo/src/main/webapp/content/030-select/80-selectManyList/SelectManyList.xhtml
new file mode 100644
index 0000000000..69294f9ec2
--- /dev/null
+++ b/tobago-example/tobago-example-demo/src/main/webapp/content/030-select/80-selectManyList/SelectManyList.xhtml
@@ -0,0 +1,87 @@
+<?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:tc="http://myfaces.apache.org/tobago/component"
+                xmlns:ui="http://xmlns.jcp.org/jsf/facelets"
+                xmlns:f="http://xmlns.jcp.org/jsf/core">
+
+  <!-- todo -->
+  <tc:badge markup="warning"
+            value="Preliminary feature, may be subject to change!"/><br/>
+
+  <tc:section label="not inline / no filter">
+    <tc:selectManyList id="selected1" value="#{selectManyListController.selected1}">
+      <tc:selectItems value="#{selectManyListController.planets}"
+                      var="planet" itemLabel="#{planet.name}" itemValue="#{planet}"/>
+    </tc:selectManyList>
+  </tc:section>
+
+  <tc:section label="inline / no filter">
+    <tc:selectManyList id="selected2" value="#{selectManyListController.selected2}" inline="true">
+      <tc:selectItems value="#{selectManyListController.planets}"
+                      var="planet" itemLabel="#{planet.name}" itemValue="#{planet}"/>
+    </tc:selectManyList>
+    <tc:button label="Submit"/>
+  </tc:section>
+
+  <tc:section label="not inline / with filter">
+    <tc:selectManyList id="selected3" value="#{selectManyListController.selected3}" filter="contains">
+      <tc:selectItems value="#{selectManyListController.planets}"
+                      var="planet" itemLabel="#{planet.name}" itemValue="#{planet}"/>
+    </tc:selectManyList>
+    <tc:button label="Submit"/>
+  </tc:section>
+
+  <tc:section label="inline / with filter:">
+    <tc:selectManyList id="selected4" value="#{selectManyListController.selected4}" filter="contains" inline="true">
+      <tc:selectItems value="#{selectManyListController.planets}"
+                      var="planet" itemLabel="#{planet.name}" itemValue="#{planet}"/>
+    </tc:selectManyList>
+    <tc:button label="Submit"/>
+  </tc:section>
+
+  <tc:section id="filter" label="Filter types: contains, startsWith, containsExact, startsWithExact">
+    <tc:selectOneRadio label="Filter type" value="#{selectManyListController.filterType}">
+      <tc:selectItem itemValue="#{null}" itemLabel="please select"/>
+      <tc:selectItem itemValue="contains"/>
+      <tc:selectItem itemValue="startsWith"/>
+      <tc:selectItem itemValue="containsExact"/>
+      <tc:selectItem itemValue="startsWithExact"/>
+      <f:ajax render="filter" execute="filter"/>
+    </tc:selectOneRadio>
+    <tc:selectManyList id="selected5" value="#{selectManyListController.selected5}"
+                       filter="#{selectManyListController.filterType}" inline="true">
+      <tc:selectItems value="#{selectManyListController.names}"
+                      var="name" itemLabel="#{name}" itemValue="#{name}"/>
+      <tc:style maxHeight="300px"/>
+    </tc:selectManyList>
+
+    Dependent of the type of the filter:
+    Ignores case.
+    Ignores acute (é), grave (è), circumflex (â, î or ô), tilde (ñ), umlaut and
+    dieresis (ü or ï), and cedilla (ç).
+  </tc:section>
+
+  <tc:section label="Filter types: custom">
+    todo
+  </tc:section>
+
+</ui:composition>
diff --git a/tobago-theme/jest.config.js b/tobago-theme/jest.config.js
index 80e4883ab1..58aebcbf99 100644
--- a/tobago-theme/jest.config.js
+++ b/tobago-theme/jest.config.js
@@ -18,5 +18,8 @@
 module.exports = {
   preset: "ts-jest",
   testEnvironment: "node",
-  rootDir: "tobago-theme-standard/"
+  rootDir: "tobago-theme-standard/",
+  globals: {
+    "window": {}
+  }
 };
diff --git a/tobago-theme/src/main/scss/_tobago.scss b/tobago-theme/src/main/scss/_tobago.scss
index e75b2f42ea..ecb0d9346c 100644
--- a/tobago-theme/src/main/scss/_tobago.scss
+++ b/tobago-theme/src/main/scss/_tobago.scss
@@ -50,6 +50,9 @@ $tobago-page-padding-top: 1rem;
 $tobago-header-margin-bottom: $tobago-page-padding-top;
 $tobago-flex-layout-spacing: 0.5rem;
 
+/* bootstrap variables --------------------------------------- */
+$form-select-disabled-color: rgba($form-select-color, $tobago-form-disabled-alpha) !default;
+
 /* utilities ----------------------------------------------------- */
 .tobago-display-inline-block {
   display: inline-block;
@@ -111,6 +114,25 @@ $tobago-flex-layout-spacing: 0.5rem;
   }
 }
 
+@mixin formControlFocusShadows() {
+  //_form-control:focus from bootstrap
+  @if $enable-shadows {
+    @include box-shadow($input-box-shadow, $input-focus-box-shadow);
+  } @else {
+    // Avoid using mixin so we can pass custom focus shadow properly
+    box-shadow: $input-focus-box-shadow;
+  }
+}
+
+@mixin formControlFocus() {
+  //_form-control:focus from bootstrap
+  color: $input-focus-color;
+  background-color: $input-focus-bg;
+  border-color: $input-focus-border-color;
+  outline: 0;
+  @include formControlFocusShadows();
+}
+
 @mixin formControlSelectListDisabled() {
   &:disabled option, option:disabled {
     color: rgba($input-color, $tobago-form-disabled-alpha);
@@ -1162,6 +1184,145 @@ tobago-select-one-radio {
   }
 }
 
+tobago-select-many-list {
+  display: block;
+
+  &.tobago-label-container {
+    .dropdown, .list-group {
+      flex: 1 0 0;
+    }
+  }
+
+  &.tobago-focus {
+    @mixin borderColorShadow($name, $color) {
+      &.border-#{$name} {
+        box-shadow: 0 0 $input-btn-focus-blur $input-focus-width rgba($color, $input-btn-focus-color-opacity);
+      }
+    }
+
+    .dropdown .tobago-select-field {
+      @include formControlFocus();
+      @include borderColorShadow("danger", $danger);
+      @include borderColorShadow("warning", $warning);
+      @include borderColorShadow("info", $info);
+    }
+
+    .list-group {
+      --bs-list-group-color: #{$input-focus-color};
+      --bs-list-group-bg: #{$input-focus-bg};
+      --bs-list-group-border-color: #{$input-focus-border-color};
+
+      @include formControlFocusShadows();
+      @include borderColorShadow("danger", $danger);
+      @include borderColorShadow("warning", $warning);
+      @include borderColorShadow("info", $info);
+    }
+  }
+
+  &.tobago-disabled {
+    .tobago-select-field {
+      color: $form-select-disabled-color;
+      background-color: $form-select-disabled-bg;
+      border-color: $form-select-disabled-border-color;
+    }
+  }
+
+  .list-group {
+    height: inherit;
+    max-height: inherit;
+    @include box-shadow($form-select-box-shadow);
+    @include transition($form-select-transition);
+
+    &.border-danger, &.border-warning, &.border-info {
+      .list-group-item {
+        border-color: inherit;
+      }
+    }
+
+    .list-group-item.tobago-select-field {
+      border-bottom-color: $list-group-border-color;
+    }
+  }
+
+  .tobago-select-field {
+    display: flex;
+    flex-wrap: wrap;
+    align-items: center;
+
+    &.dropdown-toggle::after {
+      content: none;
+    }
+
+    &.list-group-item.form-control {
+      border-bottom-left-radius: 0;
+      border-bottom-right-radius: 0;
+
+      padding: $form-select-padding-y $form-select-indicator-padding $form-select-padding-y $form-select-padding-x;
+    }
+
+    .btn-group + .tobago-filter {
+      margin-left: 0.25rem;
+    }
+
+    .btn-group {
+      margin-right: 0.25rem;
+    }
+
+    .tobago-filter {
+      margin: 0;
+      border: 0;
+      border-radius: 0;
+      padding: 0;
+      flex: 1 0 0;
+      min-width: 8rem;
+      background-color: transparent;
+
+      &:focus {
+        box-shadow: none;
+      }
+
+      &:read-only {
+        width: 0;
+        min-width: 0;
+      }
+    }
+  }
+
+  .tobago-options {
+    &.list-group-item {
+      padding: 0;
+      overflow-y: auto;
+    }
+
+    .table {
+      margin-bottom: 0;
+
+      tr {
+        cursor: pointer;
+
+        &.tobago-disabled {
+          color: $form-select-disabled-color;
+          background-color: $form-select-disabled-bg;
+          border-color: $form-select-disabled-border-color;
+          cursor: initial;
+          pointer-events: none;
+        }
+
+        &.tobago-mark {
+          --bs-table-accent-bg: var(--bs-table-hover-bg);
+          color: var(--bs-table-hover-color);
+        }
+
+        &:last-of-type {
+          td {
+            border-bottom-width: 0;
+          }
+        }
+      }
+    }
+  }
+}
+
 /* selectManyCheckbox ----------------------------------------------------- */
 tobago-select-many-checkbox {
   display: block;
diff --git a/tobago-theme/tobago-theme-standard/src/main/resources/META-INF/tobago-config.xml b/tobago-theme/tobago-theme-standard/src/main/resources/META-INF/tobago-config.xml
index e07725496b..c9faad6aed 100644
--- a/tobago-theme/tobago-theme-standard/src/main/resources/META-INF/tobago-config.xml
+++ b/tobago-theme/tobago-theme-standard/src/main/resources/META-INF/tobago-config.xml
@@ -77,6 +77,9 @@
         <tag name="selectManyCheckbox">
           <attribute name="labelLayout" default="flexLeft"/>
         </tag>
+        <tag name="selectManyList">
+          <attribute name="labelLayout" default="flexLeft"/>
+        </tag>
         <tag name="selectManyListbox">
           <attribute name="labelLayout" default="flexLeft"/>
         </tag>
diff --git a/tobago-theme/tobago-theme-standard/src/main/ts/tobago-all.ts b/tobago-theme/tobago-theme-standard/src/main/ts/tobago-all.ts
index 15bd6e250a..d9f7acd722 100644
--- a/tobago-theme/tobago-theme-standard/src/main/ts/tobago-all.ts
+++ b/tobago-theme/tobago-theme-standard/src/main/ts/tobago-all.ts
@@ -36,6 +36,7 @@ import "./tobago-reload";
 import "./tobago-scroll";
 import "./tobago-select-boolean-checkbox";
 import "./tobago-select-boolean-toggle";
+import "./tobago-select-many-list";
 import "./tobago-select-many-checkbox";
 import "./tobago-select-many-listbox";
 import "./tobago-select-many-shuttle";
diff --git a/tobago-theme/tobago-theme-standard/src/main/ts/tobago-filter-registry.test.ts b/tobago-theme/tobago-theme-standard/src/main/ts/tobago-filter-registry.test.ts
new file mode 100644
index 0000000000..c63ece9144
--- /dev/null
+++ b/tobago-theme/tobago-theme-standard/src/main/ts/tobago-filter-registry.test.ts
@@ -0,0 +1,124 @@
+/*
+ * 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 {TobagoFilterRegistry} from "./tobago-filter-registry";
+
+beforeEach(() => {
+  Object.defineProperty(window, "navigator", {value: {}, configurable: true});
+  // "es" is only one example
+  Object.defineProperty(window.navigator, "language", {value: "es", configurable: true});
+});
+
+test("a startsWith a", () => {
+  const p = TobagoFilterRegistry.get("startsWith");
+  expect(p("a", "a")).toBeTruthy();
+});
+
+test("a startsWith b", () => {
+  const p = TobagoFilterRegistry.get("startsWith");
+  expect(p("a", "b")).toBeFalsy();
+});
+
+test("a startsWith A", () => {
+  const p = TobagoFilterRegistry.get("startsWith");
+  expect(p("a", "A")).toBeTruthy();
+});
+
+test("AB startsWith á", () => {
+  const p = TobagoFilterRegistry.get("startsWith");
+  expect(p("AB", "á")).toBeTruthy();
+});
+
+test("AB contains á", () => {
+  const c = TobagoFilterRegistry.get("contains");
+  expect(c("aB", "á")).toBeTruthy();
+});
+
+test("BAB contains á", () => {
+  const c = TobagoFilterRegistry.get("contains");
+  expect(c("BAB", "á")).toBeTruthy();
+});
+
+test("El niño startsWith \u00F1", () => {
+  const c = TobagoFilterRegistry.get("startsWith");
+  expect(c("El niño", "\u00F1")).toBeFalsy();
+});
+
+test("El niño contains \u00F1", () => {
+  const c = TobagoFilterRegistry.get("contains");
+  expect(c("El niño", "\u00F1")).toBeTruthy();
+});
+
+test("El niño startsWith \u006E\u0303", () => {
+  const c = TobagoFilterRegistry.get("startsWith");
+  expect(c("El niño", "\u006E\u0303")).toBeFalsy();
+});
+
+test("El niño contains \u006E\u0303", () => {
+  const c = TobagoFilterRegistry.get("contains");
+  expect(c("El niño", "\u006E\u0303")).toBeTruthy();
+});
+
+test("El niño startsWith Ñ", () => {
+  const c = TobagoFilterRegistry.get("startsWith");
+  expect(c("El niño", "Ñ")).toBeFalsy();
+});
+
+test("El niño contains Ñ", () => {
+  const c = TobagoFilterRegistry.get("contains");
+  expect(c("El niño", "Ñ")).toBeTruthy();
+});
+
+test("Am\u00e9lie startsWith Am\u0065\u0301lie", () => {
+  const c = TobagoFilterRegistry.get("startsWith");
+  expect(c("Am\u00e9lie", "Am\u0065\u0301lie")).toBeTruthy();
+});
+
+test("Am\u00e9lie contains Am\u0065\u0301lie", () => {
+  const c = TobagoFilterRegistry.get("contains");
+  expect(c("Am\u00e9lie", "Am\u0065\u0301lie")).toBeTruthy();
+});
+
+test("Barış startsWith Baris", () => {
+  const c = TobagoFilterRegistry.get("startsWith");
+// XXX skip  expect(c("Barış", "Baris")).toBeTruthy();
+});
+
+test("Barış contains Baris", () => {
+  const c = TobagoFilterRegistry.get("contains");
+// XXX skip  expect(c("Barış", "Baris")).toBeTruthy();
+});
+
+test("Uğur startsWith Ugur", () => {
+  const c = TobagoFilterRegistry.get("startsWith");
+  expect(c("Uğur", "Ugur")).toBeTruthy();
+});
+
+test("Uğur contains Ugur", () => {
+  const c = TobagoFilterRegistry.get("contains");
+  expect(c("Uğur", "Ugur")).toBeTruthy();
+});
+
+test("Mjølnir startsWith Mjolnir", () => {
+  const c = TobagoFilterRegistry.get("startsWith");
+  expect(c("Mjølnir", "Mjolnir")).toBeTruthy();
+});
+
+test("Mjølnir contains Mjolnir", () => {
+  const c = TobagoFilterRegistry.get("contains");
+  expect(c("Mjølnir", "Mjolnir")).toBeTruthy();
+});
diff --git a/tobago-theme/tobago-theme-standard/src/main/ts/tobago-filter-registry.ts b/tobago-theme/tobago-theme-standard/src/main/ts/tobago-filter-registry.ts
new file mode 100644
index 0000000000..27d3bab0ed
--- /dev/null
+++ b/tobago-theme/tobago-theme-standard/src/main/ts/tobago-filter-registry.ts
@@ -0,0 +1,102 @@
+/*
+ * 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.
+ */
+
+export class TobagoFilterRegistry {
+
+  // todo: use "function(string, string): boolean" instead of "any"
+  private static map: Map<string, any> = new Map<string, any>();
+
+  static {
+    /**
+     * Filter for "contains" search.
+     * Ignores case.
+     * Ignores acute (é), grave (è), circumflex (â, î or ô), tilde (ñ), umlaut and
+     * dieresis (ü or ï), and cedilla (ç).
+     */
+    TobagoFilterRegistry.set("contains",
+        (item: string, search: string): boolean =>
+            TobagoFilterRegistry.localeContains(item, search, false)
+    );
+
+    /**
+     * Filter for "startsWith" search.
+     * Ignores case.
+     * Ignores acute (é), grave (è), circumflex (â, î or ô), tilde (ñ), umlaut and
+     * dieresis (ü or ï), and cedilla (ç).
+     */
+    TobagoFilterRegistry.set("startsWith",
+        (item: string, search: string): boolean =>
+            TobagoFilterRegistry.localeContains(item, search, true)
+    );
+
+    /**
+     * Filter for "containsExact" search.
+     */
+    TobagoFilterRegistry.set("containsExact",
+        (item: string, search: string): boolean =>
+            item.indexOf(search) >= 0
+    );
+
+    /**
+     * Filter for "startsWithExact" search.
+     */
+    TobagoFilterRegistry.set("startsWithExact",
+        (item: string, search: string): boolean =>
+            item.indexOf(search) == 0
+    );
+
+  }
+
+  static set(key: string, value: any): void {
+    this.map.set(key, value);
+  }
+
+  static get(key: string): any {
+    const value = this.map.get(key);
+    if (value) {
+      return value;
+    } else {
+      console.warn("TobagoFilterRegistry.get(" + key + ") = undefined");
+      return null;
+    }
+  }
+
+  private static localeContains(item: string, search: string, startOnly: boolean) {
+    item = item.normalize();
+    search = search.normalize();
+
+    const searchLength = search.length;
+    const diffLength = startOnly ? 0 : item.length - searchLength;
+
+    // console.log("a", item);
+    // console.log("b", search);
+    // console.log("diffLength", diffLength);
+
+    for (let i = 0; i <= diffLength; i++) {
+      // console.log("i", i);
+      const s = item.substring(i, i + searchLength);
+      // console.log("s", s);
+      if (s
+          .localeCompare(search, window.navigator.language, {sensitivity: "base"}) === 0) {
+        return true;
+      }
+    }
+
+    return false;
+  }
+
+}
diff --git a/tobago-theme/tobago-theme-standard/src/main/ts/tobago-select-many-list.ts b/tobago-theme/tobago-theme-standard/src/main/ts/tobago-select-many-list.ts
new file mode 100644
index 0000000000..fdc7a4cae5
--- /dev/null
+++ b/tobago-theme/tobago-theme-standard/src/main/ts/tobago-select-many-list.ts
@@ -0,0 +1,421 @@
+/*
+ * 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 {TobagoFilterRegistry} from "./tobago-filter-registry";
+import {createPopper, Instance} from "@popperjs/core";
+
+class SelectManyList extends HTMLElement {
+  private popper: Instance;
+
+  private readonly CssClass = {
+    DROPDOWN_MENU: "dropdown-menu",
+    SHOW: "show",
+    TABLE_ACTIVE: "table-active",
+    TABLE_PRIMARY: "table-primary",
+    TOBAGO_DISABLED: "tobago-disabled",
+    TOBAGO_FOCUS: "tobago-focus",
+    TOBAGO_MARK: "tobago-mark",
+    TOBAGO_OPTIONS: "tobago-options"
+  };
+
+  private readonly Key = {
+    ARROW_DOWN: "ArrowDown",
+    ARROW_UP: "ArrowUp",
+    ENTER: "Enter",
+    ESCAPE: "Escape",
+    SPACE: " "
+  };
+
+  constructor() {
+    super();
+  }
+
+  get hiddenSelect(): HTMLSelectElement {
+    return this.querySelector("select");
+  }
+
+  get selectField(): HTMLDivElement {
+    return this.querySelector(".tobago-select-field");
+  }
+
+  get badgeCloseButtons(): NodeListOf<HTMLButtonElement> {
+    return this.selectField.querySelectorAll("button.btn.badge");
+  }
+
+  get filter(): string {
+    return this.getAttribute("filter");
+  }
+
+  get filterInput(): HTMLInputElement {
+    return this.querySelector(".tobago-filter");
+  }
+
+  get dropdownMenu(): HTMLDivElement {
+    const root = this.getRootNode() as ShadowRoot | Document;
+    return root.querySelector(`.dropdown-menu[name='${this.id}']`);
+  }
+
+  get menuStore(): HTMLDivElement {
+    const root = this.getRootNode() as ShadowRoot | Document;
+    return root.querySelector(".tobago-page-menuStore");
+  }
+
+  get tbody(): HTMLElement {
+    const root = this.getRootNode() as ShadowRoot | Document;
+    return root.querySelector(`.tobago-options[name='${this.id}'] tbody`);
+  }
+
+  get enabledRows(): NodeListOf<HTMLTableRowElement> {
+    return this.tbody.querySelectorAll<HTMLTableRowElement>("tr:not(.tobago-disabled)");
+  }
+
+  get markedRow(): HTMLTableRowElement {
+    return this.tbody.querySelector<HTMLTableRowElement>("." + this.CssClass.TOBAGO_MARK);
+  }
+
+  connectedCallback(): void {
+    if (this.dropdownMenu) {
+      this.popper = createPopper(this.selectField, this.dropdownMenu, {});
+      window.addEventListener("resize", this.resizeEvent.bind(this));
+    }
+    document.addEventListener("click", this.clickEvent.bind(this));
+    this.filterInput.addEventListener("focus", this.focusEvent.bind(this));
+    this.filterInput.addEventListener("blur", this.blurEvent.bind(this));
+    this.selectField.addEventListener("keydown", this.keydownEvent.bind(this));
+
+    // init badges
+    this.querySelectorAll("option:checked").forEach(
+      option => this.sync(<HTMLOptionElement>option)
+    );
+
+    this.initList();
+
+    // init filter
+    if (this.filter) {
+      const input = this.filterInput;
+      input.addEventListener("keyup", this.filterEvent.bind(this));
+    }
+
+    // handle autofocus; trigger focus event
+    if (document.activeElement.id === this.filterInput.id) {
+      this.focusEvent();
+    }
+  }
+
+  select(event: MouseEvent): void {
+    const target = <HTMLElement>event.target;
+    const row = target.closest("tr");
+    this.selectRow(row);
+  }
+
+  selectRow(row: HTMLTableRowElement): void {
+    const itemValue = row.dataset.tobagoValue;
+    console.info("itemValue", itemValue);
+    const select = this.hiddenSelect;
+    const option: HTMLOptionElement = select.querySelector(`[value="${itemValue}"]`);
+    option.selected = !option.selected;
+    this.sync(option);
+  }
+
+  removeBadge(event: MouseEvent): void {
+    const target = <HTMLElement>event.target;
+    const group: HTMLElement = target.closest(".btn-group");
+    const itemValue = group.dataset.tobagoValue;
+    const select = this.hiddenSelect;
+    const option: HTMLOptionElement = select.querySelector(`[value="${itemValue}"]`);
+    option.selected = false;
+    this.sync(option);
+  }
+
+  sync(option: HTMLOptionElement) {
+    const itemValue = option.value;
+    const row: HTMLTableRowElement = this.tbody.querySelector(`[data-tobago-value="${itemValue}"]`);
+    if (option.selected) {
+      // create badge
+      const tabIndex: number = this.filterInput.tabIndex;
+      this.filterInput.insertAdjacentHTML("beforebegin",
+        this.getRowTemplate(itemValue, row.innerText, option.disabled || this.hiddenSelect.disabled, tabIndex));
+
+      // todo: nicer adding the @click with lit-html
+      const closeButton = this.selectField
+        .querySelector(".btn-group[data-tobago-value='" + itemValue + "'] button.btn.badge");
+      closeButton?.addEventListener("click", this.removeBadge.bind(this));
+      closeButton?.addEventListener("focus", this.focusEvent.bind(this));
+      closeButton?.addEventListener("blur", this.blurEvent.bind(this));
+
+      // highlight list row
+      row.classList.add(this.CssClass.TABLE_PRIMARY);
+    } else {
+      // remove badge
+      const badge = this.selectField.querySelector(`[data-tobago-value="${itemValue}"]`);
+      const previousBadge = badge.previousElementSibling;
+      const nextBadge = badge.nextElementSibling.tagName === "SPAN" ? badge.nextElementSibling : null;
+      badge.remove();
+      if (previousBadge) {
+        previousBadge.querySelector<HTMLButtonElement>("button.btn.badge").focus();
+      } else if (nextBadge) {
+        nextBadge.querySelector<HTMLButtonElement>("button.btn.badge").focus();
+      } else {
+        this.filterInput.disabled = false;
+        this.filterInput.focus();
+      }
+
+      // remove highlight list row
+      row.classList.remove(this.CssClass.TABLE_PRIMARY);
+    }
+
+    if (!this.classList.contains(this.CssClass.TOBAGO_DISABLED) && !this.filter) {
+      // disable input field to prevent focus.
+      if (this.badgeCloseButtons.length > 0 && this.filterInput.id === document.activeElement.id) {
+        this.badgeCloseButtons.item(this.badgeCloseButtons.length - 1).focus();
+      }
+      this.filterInput.disabled = this.badgeCloseButtons.length > 0;
+    }
+  }
+
+  getRowTemplate(value: string, text: string, disabled: boolean, tabIndex: number): string {
+    return disabled ? `
+<span class="btn-group" role="group" data-tobago-value="${value}">
+  <tobago-badge class="badge text-bg-primary btn disabled">${text}</tobago-badge>
+</span>` : `
+<span class="btn-group" role="group" data-tobago-value="${value}">
+  <tobago-badge class="badge text-bg-primary btn">${text}</tobago-badge>
+  <button type='button' class='tobago-button btn btn-secondary badge'
+  ${tabIndex > 0 ? " tabindex='" + String(tabIndex) + "'" : ""}><i class='bi-x-lg'></i></button>
+</span>`;
+  }
+
+  filterEvent(event: Event): void {
+    const input = event.currentTarget as HTMLInputElement;
+    const searchString = input.value;
+    console.info("searchString", searchString);
+    const filterFunction = TobagoFilterRegistry.get(this.filter);
+    // XXX todo: if filterFunction not found?
+    if (filterFunction != null) {
+      this.querySelectorAll("tr").forEach(row => {
+        const itemValue = row.dataset.tobagoValue;
+        if (filterFunction(itemValue, searchString)) {
+          row.classList.remove("d-none");
+        } else {
+          row.classList.add("d-none");
+        }
+      });
+    }
+  }
+
+  private clickEvent(event: MouseEvent): void {
+    if (this.isDeleted(event.target as Element)) {
+      // do nothing, this is probably a removed badge
+    } else if (this.isPartOfSelectField(event.target as Element)
+      || this.isPartOfTobagoOptions(event.target as Element)) {
+
+      if (!this.filterInput.disabled) {
+        this.filterInput.focus();
+      } else if (this.badgeCloseButtons.length > 0) {
+        this.badgeCloseButtons[0].focus();
+      }
+
+    } else {
+      this.hideDropdown();
+      this.setFocus(false);
+    }
+  }
+
+  private keydownEvent(event: KeyboardEvent) {
+    switch (event.key) {
+      case this.Key.ESCAPE:
+        this.hideDropdown();
+        this.removeTableRowMark();
+        break;
+      case this.Key.ARROW_DOWN:
+        event.preventDefault();
+        this.showDropdown();
+        this.markNextTableRow();
+        break;
+      case this.Key.ARROW_UP:
+        event.preventDefault();
+        this.showDropdown();
+        this.markPreviousTableRow();
+        break;
+      case this.Key.ENTER:
+      case this.Key.SPACE:
+        if (this.markedRow) {
+          event.preventDefault();
+          this.selectMarkedOption();
+        } else if (document.activeElement.id === this.filterInput.id) {
+          this.showDropdown();
+        }
+        break;
+    }
+  }
+
+  private markNextTableRow(): void {
+    const rows = this.enabledRows;
+    const indexOfMark = this.indexOfTobagoMark(rows);
+    if (indexOfMark >= 0) {
+      if (indexOfMark + 1 < rows.length) {
+        rows.item(indexOfMark).classList.remove(this.CssClass.TOBAGO_MARK);
+        this.addTableRowMark(rows.item(indexOfMark + 1));
+      } else {
+        rows.item(rows.length - 1).classList.remove(this.CssClass.TOBAGO_MARK);
+        this.addTableRowMark(rows.item(0));
+      }
+    } else if (rows.length > 0) {
+      this.addTableRowMark(rows.item(0));
+    }
+  }
+
+  private markPreviousTableRow(): void {
+    const rows = this.enabledRows;
+    const indexOfMark = this.indexOfTobagoMark(rows);
+    if (indexOfMark >= 0) {
+      if ((indexOfMark - 1) >= 0) {
+        rows.item(indexOfMark).classList.remove(this.CssClass.TOBAGO_MARK);
+        this.addTableRowMark(rows.item(indexOfMark - 1));
+      } else {
+        rows.item(0).classList.remove(this.CssClass.TOBAGO_MARK);
+        this.addTableRowMark(rows.item(rows.length - 1));
+      }
+    } else if (rows.length > 0) {
+      this.addTableRowMark(rows.item(rows.length - 1));
+    }
+  }
+
+  private addTableRowMark(row: HTMLTableRowElement): void {
+    row.classList.add(this.CssClass.TOBAGO_MARK);
+    if (!this.dropdownMenu) {
+      row.scrollIntoView({block: "center"});
+    }
+  }
+
+  private removeTableRowMark(): void {
+    this.markedRow?.classList.remove(this.CssClass.TOBAGO_MARK);
+  }
+
+  private selectMarkedOption(): void {
+    const row = this.tbody.querySelector<HTMLTableRowElement>("." + this.CssClass.TOBAGO_MARK);
+    this.selectRow(row);
+  }
+
+  private indexOfTobagoMark(rows: NodeListOf<HTMLTableRowElement>): number {
+    for (let i = 0; i < rows.length; i++) {
+      if (rows.item(i).classList.contains(this.CssClass.TOBAGO_MARK)) {
+        return i;
+      }
+    }
+    return -1;
+  }
+
+  private isPartOfSelectField(element: Element): boolean {
+    if (element) {
+      if (this.selectField.id === element.id) {
+        return true;
+      } else {
+        return element.parentElement ? this.isPartOfSelectField(element.parentElement) : false;
+      }
+    } else {
+      return false;
+    }
+  }
+
+  private isPartOfTobagoOptions(element: Element): boolean {
+    if (element) {
+      if (element.classList.contains(this.CssClass.TOBAGO_OPTIONS)
+        && this.id === element.getAttribute("name")) {
+        return true;
+      } else {
+        return element.parentElement ? this.isPartOfTobagoOptions(element.parentElement) : false;
+      }
+    } else {
+      return false;
+    }
+  }
+
+  private isDeleted(element: Element): boolean {
+    return element.closest("html") === null;
+  }
+
+  private showDropdown(): void {
+    if (this.dropdownMenu && !this.dropdownMenu.classList.contains(this.CssClass.SHOW)) {
+      this.selectField.classList.add(this.CssClass.SHOW);
+      this.selectField.ariaExpanded = "true";
+      this.dropdownMenu.classList.add(this.CssClass.SHOW);
+      this.updateDropdownMenuWidth();
+      this.popper.update();
+    }
+  }
+
+  private hideDropdown(): void {
+    if (this.dropdownMenu?.classList.contains(this.CssClass.SHOW)) {
+      this.selectField.classList.remove(this.CssClass.SHOW);
+      this.selectField.ariaExpanded = "false";
+      this.dropdownMenu.classList.remove(this.CssClass.SHOW);
+    }
+  }
+
+  private resizeEvent(event: UIEvent): void {
+    this.updateDropdownMenuWidth();
+  }
+
+  private updateDropdownMenuWidth(): void {
+    if (this.dropdownMenu) {
+      this.dropdownMenu.style.width = `${this.selectField.offsetWidth}px`;
+    }
+  }
+
+  private focusEvent(): void {
+    if (!this.hiddenSelect.disabled) {
+      if (!this.classList.contains(this.CssClass.TOBAGO_FOCUS)) {
+        this.setFocus(true);
+        this.showDropdown();
+      }
+    }
+  }
+
+  private blurEvent(event: FocusEvent): void {
+    if (event.relatedTarget !== null) {
+      //relatedTarget is the new focused element; null indicate a mouseclick or an inactive browser window
+      if (!this.isPartOfSelectField(event.relatedTarget as Element)
+        && !this.isPartOfTobagoOptions(event.relatedTarget as Element)) {
+        this.setFocus(false);
+        this.hideDropdown();
+      }
+    }
+  }
+
+  private setFocus(focus: boolean): void {
+    if (focus) {
+      this.classList.add(this.CssClass.TOBAGO_FOCUS);
+    } else {
+      this.classList.remove(this.CssClass.TOBAGO_FOCUS);
+    }
+  }
+
+  private initList() {
+    const tbody = this.tbody;
+    tbody.addEventListener("click", this.select.bind(this));
+    tbody.querySelectorAll("tr").forEach((row: HTMLTableRowElement) => {
+      // row stuff
+    });
+  }
+}
+
+document.addEventListener("tobago.init", function (event: Event): void {
+  if (window.customElements.get("tobago-select-many-list") == null) {
+    window.customElements.define("tobago-select-many-list", SelectManyList);
+  }
+});