Repository: it-at-m/muc-patternlab-vue
Pull request #426: 410 mucselect accessibility
Author: login: lehju
Labels: name: released
name: released on @beta
Created: 2025-03-04T09:17:55Z | Merged: 2025-03-05T12:11:33Z

**Description**

Add choices.js to get tabindex and aria-labels on select component.
Fix padding and margin bugs in button component.

**Reference**

closes #410


<!-- This is an auto-generated comment: release notes by coderabbit.ai -->

## Summary by CodeRabbit

- **New Features**
  - Enhanced the multi-selection experience with improved placeholder options and dynamic item selection.
  - Refined button appearance with optimized icon positioning and margin adjustments.
  
- **Refactor**
  - Streamlined the select component by replacing custom input elements with a native select element enhanced by an external library.
  - Removed legacy item rendering components to simplify the selection interface.
  
- **Chores**
  - Incorporated a new external dependency to support enhanced select functionality.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->

Linked issue #410: MucSelect accessibility
**Is your feature request related to a problem? Please describe.**
The select component is not designed for screen reader.

**Describe the solution you'd like**
Add tabindex and aria-labels to select component.

Linked issue comments: user: login: github-actions[bot]
created_at: 2025-03-05T12:15:05Z
body: :tada: This issue has been resolved in version 4.0.0-beta.2 :tada:

The release is available on:
- [npm package (@beta dist-tag)](https://www.npmjs.com/package/@muenchen/muc-patternlab-vue/v/4.0.0-beta.2)
- [GitHub release](https://github.com/it-at-m/muc-patternlab-vue/releases/tag/v4.0.0-beta.2)

Your **[semantic-release](https://github.com/semantic-release/semantic-release)** bot :package::rocket:
author_association: NONE
user: login: github-actions[bot]
created_at: 2025-03-11T12:35:02Z
body: :tada: This issue has been resolved in version 4.0.0 :tada:

The release is available on:
- [npm package (@latest dist-tag)](https://www.npmjs.com/package/@muenchen/muc-patternlab-vue/v/4.0.0)
- [GitHub release](https://github.com/it-at-m/muc-patternlab-vue/releases/tag/v4.0.0)

Your **[semantic-release](https://github.com/semantic-release/semantic-release)** bot :package::rocket:
author_association: NONE

Diff:
diff --git a/package-lock.json b/package-lock.json
index 9beedeb6..e9fc66fd 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -11,6 +11,7 @@
       "license": "MIT",
       "dependencies": {
         "@splidejs/vue-splide": "0.6.12",
+        "choices.js": "11.0.3",
         "patch-package": "8.0.0"
       },
       "devDependencies": {
@@ -4718,6 +4719,14 @@
         "node": ">= 16"
       }
     },
+    "node_modules/choices.js": {
+      "version": "11.0.3",
+      "integrity": "sha512-sn1oLUEcvjj7vSSIT0QyexmLIeD6PFGSWrUUXKShL2LUtVFXU3OAIY/smNIQg0OKav3yk1rFa+F56hZ/uYC4cg==",
+      "license": "MIT",
+      "dependencies": {
+        "fuse.js": "^7.0.0"
+      }
+    },
     "node_modules/chownr": {
       "version": "2.0.0",
       "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==",
@@ -6591,6 +6600,14 @@
         "url": "https://github.com/sponsors/sindresorhus"
       }
     },
+    "node_modules/fuse.js": {
+      "version": "7.1.0",
+      "integrity": "sha512-trLf4SzuuUxfusZADLINj+dE8clK1frKdmqiJNb1Es75fmI5oY6X2mxLVUciLLjxqw/xr72Dhy+lER6dGd02FQ==",
+      "license": "Apache-2.0",
+      "engines": {
+        "node": ">=10"
+      }
+    },
     "node_modules/get-caller-file": {
       "version": "2.0.5",
       "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==",
diff --git a/package.json b/package.json
index 1aa4c704..659ed229 100644
--- a/package.json
+++ b/package.json
@@ -78,6 +78,7 @@
   },
   "dependencies": {
     "@splidejs/vue-splide": "0.6.12",
+    "choices.js": "11.0.3",
     "patch-package": "8.0.0"
   }
 }
diff --git a/src/components/Button/MucButton.vue b/src/components/Button/MucButton.vue
index d2d116e6..c624ff86 100644
--- a/src/components/Button/MucButton.vue
+++ b/src/components/Button/MucButton.vue
@@ -99,7 +99,8 @@ const iconAnimatedClass = computed(() => {
 
 const iconPositionClass: Object = computed(() => ({
   "set-right-margin": iconShownLeft,
-  "no-left-margin": !iconShownLeft ? !slots.default : !iconAnimated,
+  "set-left-margin": !iconShownLeft && slots.default,
+  "no-left-margin": iconShownLeft && !iconAnimated,
 }));
 
 /**
@@ -124,6 +125,14 @@ const disabledClass = computed(() => (disabled ? "disabled" : ""));
   margin-right: 0.75rem;
 }
 
+.set-left-margin {
+  margin-left: 0.75rem;
+}
+
+.m-button--ghost {
+  padding-right: 0;
+}
+
 [aria-disabled="true"] {
   pointer-events: none;
 }
diff --git a/src/components/Form/MucSelect.stories.ts b/src/components/Form/MucSelect.stories.ts
index 6887f790..c7ecdf99 100644
--- a/src/components/Form/MucSelect.stories.ts
+++ b/src/components/Form/MucSelect.stories.ts
@@ -55,6 +55,18 @@ export const Default = {
 export const MultiSelect = {
   args: {
     ...Default.args,
+    modelValue: [
+      {
+        id: "1",
+        name: "Object 1",
+        quantity: 1,
+      },
+      {
+        id: "3",
+        name: "Object 3",
+        quantity: 3,
+      },
+    ],
     id: "multi-select",
     label: "Select multiple objects",
     multiple: true,
@@ -70,3 +82,35 @@ export const StringSelect = {
     hint: "This is a hint",
   },
 };
+
+export const Placeholder = {
+  args: {
+    id: "placeholder",
+    items: [
+      {
+        id: "1",
+        name: "Object 1",
+        quantity: 1,
+      },
+      {
+        id: "2",
+        name: "Object 2",
+        quantity: 2,
+      },
+      {
+        id: "3",
+        name: "Object 3",
+        quantity: 3,
+      },
+      {
+        id: "4",
+        name: "Object 4",
+        quantity: 4,
+      },
+    ],
+    label: "This is a label",
+    hint: "This is a hint",
+    itemTitle: "name",
+    placeholder: "Placeholder",
+  },
+};
diff --git a/src/components/Form/MucSelect.vue b/src/components/Form/MucSelect.vue
index cc196fe6..a10fc93b 100644
--- a/src/components/Form/MucSelect.vue
+++ b/src/components/Form/MucSelect.vue
@@ -7,6 +7,7 @@
       v-if="label"
       :for="'select-' + id"
       class="m-label"
+      tabindex="0"
     >
       {{ label }}
     </label>
@@ -14,73 +15,28 @@
       v-if="hint"
       class="m-hint"
       :id="'select-hint-' + id"
+      tabindex="0"
     >
       {{ hint }}
     </p>
-    <div
-      class="m-input-wrapper"
-      :class="selectType"
-    >
-      <input
-        :id="'select-' + id"
-        type="text"
-        class="m-input m-combobox m-combobox--single"
-        :aria-describedby="hint ? 'select-hint-' + id : undefined"
-        v-model="searchValue"
-        @click="openItemList"
-      />
-      <span
-        class="m-input__trigger"
-        @click="toggleItemList"
-      >
-        <svg
-          aria-hidden="true"
-          class="icon"
-        >
-          <use xlink:href="#icon-chevron-down"></use>
-        </svg>
-        <span class="visually-hidden">Auswahlliste öffnen</span>
-      </span>
-      <ul
-        class="listbox"
-        :class="displayOptions"
-        @mouseleave="emptyActiveItem"
-      >
-        <li
-          v-for="(option, index) in displayedItems"
-          :key="index"
-          class="option"
-          @mouseenter="activeItem = option"
-          :class="[isActiveClass(option), isSelectedClass(option)]"
-          @click="clicked(option)"
-        >
-          <MucSelectItem
-            :item="option"
-            :item-label="itemTitle"
-          />
-        </li>
-        <li
-          v-if="noItemsFound"
-          class="option"
-        >
-          {{ noItemFoundMessage }}
-        </li>
-      </ul>
-    </div>
+    <select
+      ref="elementRef"
+      :id="'select-' + id"
+      :aria-describedby="hint ? 'select-hint-' + id : undefined"
+      class="m-select"
+      :multiple="multiple"
+    />
   </div>
 </template>
 
 <script setup lang="ts">
-import { computed, ref, useTemplateRef, watch } from "vue";
+import Choices from "choices.js";
+import { computed, onBeforeUnmount, onMounted, ref, watch } from "vue";
 
-import useOnClickOutside from "../../composables/useOnClickOutside";
-import MucSelectItem from "./MucSelectItem.vue";
-import { ItemAsObject, MucSelectItemTypes } from "./MucSelectTypes";
+import { ChoiceType, ItemAsObject, MucSelectItemTypes } from "./MucSelectTypes";
 
-/**
- * Ref ot the component
- */
-const selectComponent = useTemplateRef("selectComponent");
+const elementRef = ref<HTMLSelectElement>();
+const choicesInstance = ref<Choices>();
 
 /**
  * Exposed selected value / values
@@ -92,31 +48,12 @@ const selectedValues = defineModel<MucSelectItemTypes | MucSelectItemTypes[]>(
   }
 );
 
-/**
- * If list of items is shown
- */
-const showItems = ref<boolean>(false);
-
-/**
- * Last interacted item - selected or deselected
- */
-const lastClickedItem = ref<MucSelectItemTypes>();
-
-/**
- * If no items found after filtering
- */
-const noItemsFound = ref<boolean>(false);
-
-/**
- * Index of currently actively hovered item or selected item
- */
-const activeItem = ref<MucSelectItemTypes>();
-
 const {
   items,
   multiple = false,
   noItemFoundMessage = "No items found.",
   itemTitle = "title",
+  placeholder,
 } = defineProps<{
   /**
    * Unique identifier for the select. Required property used to associate the select with its label and hint text for accessibility.
@@ -151,199 +88,156 @@ const {
    * Property that contains the value to be displayed in the list when a list of objects is used
    */
   itemTitle?: string;
-}>();
-
-/**
- * Toggles the list of items and sets the previously selected item as active
- */
-const toggleItemList = () => {
-  showItems.value = !showItems.value;
-  activeItem.value = lastClickedItem.value;
-};
 
-/**
- * Opens the list of items and sets the previously selected item as active
- */
-const openItemList = () => {
-  showItems.value = true;
-  activeItem.value = lastClickedItem.value;
-  searchValue.value = "";
-};
+  /**
+   * Optional placeholder shown on the input
+   */
+  placeholder?: string;
+}>();
 
 /**
- * Closes the list after clicking outside the component
+ * Creates an array with the displayed labels.
  */
-useOnClickOutside(selectComponent, () => {
-  showItems.value = false;
-  searchValue.value = outputTransformed.value;
+const displayedLabels = computed(() => {
+  let displayedItems: string[] = [];
+  items.forEach((item) => {
+    if (typeof item === "string") {
+      displayedItems.push(item);
+    } else {
+      displayedItems.push(item[itemTitle]);
+    }
+  });
+  return displayedItems;
 });
 
-/**
- * Actions upon clicking an item
- * @param clickedValue clicked item value
- */
-const clicked = (clickedValue: MucSelectItemTypes) => {
-  lastClickedItem.value = clickedValue;
-
-  multiple ? updateMVMultiple(clickedValue) : updateMVSingle(clickedValue);
+watch(
+  () => [
+    displayedLabels.value,
+    multiple,
+    noItemFoundMessage,
+    itemTitle,
+    placeholder,
+  ],
+  () => {
+    if (choicesInstance.value) {
+      choicesInstance.value.destroy();
+      createChoicesInstance();
+    }
+  }
+);
 
-  if (!multiple) showItems.value = false;
+/**
+ * Creates a new instance of Choices in which the current configuration is set.
+ * Must be executed for every change, as some of the values cannot be changed on the existing instance.
+ */
+const createChoicesInstance = () => {
+  choicesInstance.value = new Choices(elementRef.value, {
+    allowHTML: true,
+    itemSelectText: "",
+    noResultsText: noItemFoundMessage,
+    placeholder: !!placeholder,
+    placeholderValue: placeholder,
+    removeItemButton: multiple,
+    searchFields: ["label"],
+  });
+  addChoices();
+  setDefaultSelectedValue();
+  choicesInstance.value.passedElement.element.addEventListener(
+    "change",
+    updateSelectedValues
+  );
 };
 
 /**
- * Update the modelValue with the given. Performs conversion to string if necessary.
- * @param newValue the new value
- */
-const updateMVSingle = (newValue: MucSelectItemTypes) => {
-  if (Array.isArray(selectedValues.value))
-    selectedValues.value = selectedValues.value.join(" ");
-
-  if (
-    typeof selectedValues.value !== "string" &&
-    typeof newValue !== "string"
-  ) {
-    selectedValues.value =
-      selectedValues.value[itemTitle] === newValue[itemTitle] ? "" : newValue;
-  } else {
-    selectedValues.value = selectedValues.value === newValue ? "" : newValue;
+ * Replaces the selection options in the existing instance of Choices.
+ */
+const addChoices = () => {
+  if (choicesInstance.value) {
+    choicesInstance.value.clearChoices();
+    let newChoices: ChoiceType[] = [];
+    displayedLabels.value.forEach((item, index) => {
+      newChoices.push({
+        value: index.toString(),
+        label: item,
+        selected: false,
+        disabled: false,
+      });
+    });
+    choicesInstance.value.setChoices(newChoices);
   }
 };
 
 /**
- * Update the modelValue with the given. Performs conversion to array if necessary.
- * @param newValue the new value
+ * Updates the modelValue when the selected option changes.
  */
-const updateMVMultiple = (newValue: MucSelectItemTypes) => {
-  if (!Array.isArray(selectedValues.value))
-    selectedValues.value = [selectedValues.value];
-
-  if (Array.isArray(selectedValues.value)) {
-    if (typeof newValue === "string") {
-      selectedValues.value = selectedValues.value
-        .map((item: string) => item)
-        .includes(newValue)
-        ? selectedValues.value.filter((val: string) => val !== newValue)
-        : [...selectedValues.value, newValue];
+const updateSelectedValues = () => {
+  if (choicesInstance.value) {
+    let values = choicesInstance.value.getValue(true);
+    if (Array.isArray(values)) {
+      let selectedItems: MucSelectItemTypes[] = [];
+      values.forEach((item) => selectedItems.push(items[parseInt(item)]));
+      selectedValues.value = selectedItems;
     } else {
-      selectedValues.value = selectedValues.value
-        .map((item: ItemAsObject) => item[itemTitle])
-        .includes(newValue[itemTitle])
-        ? selectedValues.value.filter(
-            (val: ItemAsObject) => val[itemTitle] !== newValue[itemTitle]
-          )
-        : [...selectedValues.value, newValue];
+      selectedValues.value = items[parseInt(values)];
     }
   }
 };
 
-/**
- * Converts the displayed text depending on the selection mode
- */
-const outputTransformed = computed(() => {
-  if (typeof selectedValues.value === "string") {
-    return selectedValues.value;
-  } else if (!Array.isArray(selectedValues.value)) {
-    return selectedValues.value[itemTitle].toString();
-  } else if (
-    selectedValues.value.every((item: any) => typeof item === "string")
-  ) {
-    return selectedValues.value.join(multiple ? ", " : " ");
-  } else {
-    return selectedValues.value
-      .map((item) => item[itemTitle].toString())
-      .join(multiple ? ", " : " ");
+watch(
+  () => selectedValues.value,
+  () => {
+    setDefaultSelectedValue();
   }
-});
-
-watch(outputTransformed, (newOutput) => {
-  searchValue.value = newOutput;
-});
-
-/**
- * Current search value
- */
-const searchValue = ref<string>(outputTransformed.value);
-
-/**
- * Determines whether all or only the searched elements are displayed
- */
-const displayedItems = computed(() =>
-  searchValue.value == outputTransformed.value
-    ? items
-    : updateDisplayedItems(searchValue.value)
 );
 
 /**
- * Filters the list of elements after entering the search string
- * @param search the search string
- * @return list of searched items
- */
-const updateDisplayedItems = (search: string) => {
-  noItemsFound.value = false;
-  const filteredItems = items.every((item: any) => typeof item === "string")
-    ? items.filter((item) => item.toLowerCase().includes(search.toLowerCase()))
-    : items.filter((item: any) =>
-        item[itemTitle].toString().toLowerCase().includes(search.toLowerCase())
-      );
-  if (filteredItems.length === 0) {
-    noItemsFound.value = true;
+ * Sets the preselected option in the current instance of Choices.
+ */
+const setDefaultSelectedValue = () => {
+  if (choicesInstance.value) {
+    if (selectedValues.value && selectedValues.value.length != 0) {
+      if (Array.isArray(selectedValues.value)) {
+        let selectedItems: string[] = [];
+        selectedValues.value.forEach((value) =>
+          selectedItems.push(
+            items
+              .findIndex((item) => {
+                if (typeof item === "string") {
+                  return item == value;
+                } else {
+                  return item[itemTitle] == value[itemTitle];
+                }
+              })
+              .toString()
+          )
+        );
+        choicesInstance.value.setChoiceByValue(selectedItems);
+      } else {
+        const itemId = items.findIndex((item) => {
+          if (typeof item === "string") {
+            return item == selectedValues.value;
+          } else {
+            const value = selectedValues.value as ItemAsObject;
+            return item[itemTitle] == value[itemTitle];
+          }
+        });
+        choicesInstance.value.setChoiceByValue(itemId.toString());
+      }
+    } else {
+      choicesInstance.value.setChoiceByValue("");
+    }
   }
-  return filteredItems;
 };
 
-/**
- * Apply active class to hovered item
- * @param value of item
- */
-const isActiveClass = (value: MucSelectItemTypes) =>
-  value === activeItem.value ? "active" : "";
-
-/**
- * Apply selected class to selected items
- * @param value of item
- */
-const isSelectedClass = (value: MucSelectItemTypes) => {
-  if (typeof value === "string")
-    return selectedValues.value.includes(value) ? "selected" : "";
-
-  if (Array.isArray(selectedValues.value)) {
-    return selectedValues.value
-      .map((item) => item[itemTitle])
-      .includes(value[itemTitle])
-      ? "selected"
-      : "";
+onMounted(() => {
+  if (elementRef.value) {
+    createChoicesInstance();
   }
-  if (typeof selectedValues.value !== "string") {
-    return selectedValues.value[itemTitle] === value[itemTitle]
-      ? "selected"
-      : "";
-  }
-};
-
-/**
- * Resets the currently activeItem
- */
-const emptyActiveItem = () => (activeItem.value = "");
-
-/**
- * Display the list of item by changing the css-display property
- */
-const displayOptions = computed(() =>
-  showItems.value ? "display-listbox" : ""
-);
+});
 
-/**
- * Switches between the selection modes according to multiple. Checkboxes are shown on the multiple select
- */
-const selectType = computed(() =>
-  multiple && !noItemsFound.value
-    ? "m-input-wrapper--multiselect multiselect"
-    : "m-input-wrapper--select"
-);
+onBeforeUnmount(() => {
+  if (choicesInstance.value) {
+    choicesInstance.value.destroy();
+  }
+});
 </script>
-
-<style scoped>
-.display-listbox {
-  display: block !important;
-}
-</style>
diff --git a/src/components/Form/MucSelectItem.vue b/src/components/Form/MucSelectItem.vue
deleted file mode 100644
index 2ccce3fe..00000000
--- a/src/components/Form/MucSelectItem.vue
+++ /dev/null
@@ -1,29 +0,0 @@
-<template>
-  <span>
-    {{ itemText }}
-  </span>
-</template>
-
-<script setup lang="ts">
-import { computed } from "vue";
-
-import { MucSelectItemTypes } from "./MucSelectTypes";
-
-const { item, itemLabel } = defineProps<{
-  /**
-   * Individual item to display inside the MucSelect component
-   */
-  item: MucSelectItemTypes;
-  /**
-   * A key of the `item` object, only required
-   */
-  itemLabel?: string;
-}>();
-
-/**
- * Defines the displayed value
- */
-const itemText = computed(() =>
-  typeof item === "string" ? item : item[itemLabel!].toString()
-);
-</script>
diff --git a/src/components/Form/MucSelectTypes.ts b/src/components/Form/MucSelectTypes.ts
index 0ce7218d..9ca344e4 100644
--- a/src/components/Form/MucSelectTypes.ts
+++ b/src/components/Form/MucSelectTypes.ts
@@ -6,3 +6,10 @@ export interface ItemAsObject {
  * Different possible types of items
  */
 export type MucSelectItemTypes = string | ItemAsObject;
+
+export type ChoiceType = {
+  value: string;
+  label: string;
+  selected: boolean;
+  disabled: boolean;
+};


Reviews:
user: login: coderabbitai[bot]
body: **Actionable comments posted: 1**

<details>
<summary>🧹 Nitpick comments (3)</summary><blockquote>

<details>
<summary>src/components/Form/MucSelect.vue (3)</summary><blockquote>

`20-26`: **Consider adding a fallback for `aria-label` when `label` is not provided.**  
Thank you for making the `<select>` element accessible with `aria-describedby`. For additional screen reader clarity, you may want to assign an `aria-label` on the `<select>` element itself if `label` is undefined, ensuring the field has a descriptive name at all times.

---

`54-54`: **Placeholder usage looks good.**  
Your approach effectively leverages Choices.js to display placeholders, but remember placeholders alone are not always screen reader friendly. Consider clarifying the placeholder usage in your documentation for consistent user guidance.

---

`178-212`: **Consider adding more robust matching for objects during selection.**  
Currently, the matching logic compares `typeof item === "string"` or checks `item[itemTitle]`. If the underlying data changes, a mismatch might happen. You might want to unify how the item uniqueness is tracked—by ID, for example.

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: .coderabbit.yaml**
**Review profile: CHILL**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 6a381b38ac176a4d8b80ae1890f7c6a0ec72ad24 and 41ae1dcef729cb5fc457dd570e8054ce5d9571f2.

</details>

<details>
<summary>⛔ Files ignored due to path filters (1)</summary>

* `package-lock.json` is excluded by `!**/package-lock.json`

</details>

<details>
<summary>📒 Files selected for processing (6)</summary>

* `package.json` (1 hunks)
* `src/components/Button/MucButton.vue` (2 hunks)
* `src/components/Form/MucSelect.stories.ts` (2 hunks)
* `src/components/Form/MucSelect.vue` (3 hunks)
* `src/components/Form/MucSelectItem.vue` (0 hunks)
* `src/components/Form/MucSelectTypes.ts` (1 hunks)

</details>

<details>
<summary>💤 Files with no reviewable changes (1)</summary>

* src/components/Form/MucSelectItem.vue

</details>

<details>
<summary>🧰 Additional context used</summary>

<details>
<summary>🧠 Learnings (1)</summary>

<details>
<summary>src/components/Button/MucButton.vue (3)</summary>

```
Learnt from: FabianWilms
PR: it-at-m/muc-patternlab-vue#370
File: src/components/Button/MucButton.vue:95-103
Timestamp: 2025-01-14T06:43:07.219Z
Learning: In the MucButton component, animation classes like `m-button--animated-left` and `m-button--animated-right` are defined in external CSS files rather than within the component's scoped styles, following the project's CSS architecture pattern.
```

```
Learnt from: FabianWilms
PR: it-at-m/muc-patternlab-vue#370
File: src/components/Button/MucButton.vue:95-103
Timestamp: 2025-01-14T06:43:07.219Z
Learning: In the MucButton component, animation classes like `m-button--animated-left` and `m-button--animated-right` are defined in the global stylesheet at `public/assets/temporary/muenchende-style.css`, following the project's CSS architecture pattern of keeping animations in a centralized location.
```

```
Learnt from: FabianWilms
PR: it-at-m/muc-patternlab-vue#370
File: src/components/Button/MucButton.vue:95-103
Timestamp: 2025-01-14T06:43:07.219Z
Learning: The MucButton component's animation classes (`m-button--animated-left` and `m-button--animated-right`) are defined in the global stylesheet at `public/assets/temporary/muenchende-style.css`, following the project's pattern of centralizing animations in shared stylesheets rather than component-scoped styles.
```

</details>

</details>

</details>

<details>
<summary>🔇 Additional comments (16)</summary><blockquote>

<details>
<summary>package.json (1)</summary>

`81-81`: **Great addition of Choices.js! 👍**

Adding Choices.js is an excellent choice for enhancing the select component's accessibility. This dependency will help with implementing tabindex and aria-labels as mentioned in the PR objectives, which is really important for making the application more accessible to all users.

</details>
<details>
<summary>src/components/Form/MucSelectTypes.ts (1)</summary>

`10-15`: **Well-structured type definition for Choices.js integration!**

The `ChoiceType` definition looks perfect for working with Choices.js. It properly defines all the required properties with appropriate types to support the enhanced select functionality. This will make your TypeScript integration much more robust and provide good type checking.

</details>
<details>
<summary>src/components/Form/MucSelect.stories.ts (2)</summary>

`58-69`: **Great example of multi-selection capability!**

This update to provide a pre-populated `modelValue` array for the MultiSelect example is really helpful. It clearly demonstrates how the component handles multiple selections, which is a valuable reference for developers using this component.

---

`86-116`: **Excellent addition of the Placeholder example!**

Thank you for adding this Placeholder example! It nicely demonstrates how to use the placeholder feature with the select component, providing a complete usage example that will help other developers understand how to implement this functionality in their own code.

</details>
<details>
<summary>src/components/Button/MucButton.vue (3)</summary>

`102-103`: **Nice improvement to icon margin handling!**

These changes to the computed properties for icon positioning classes are a great enhancement. Adding the condition to apply the left margin only when appropriate (when the icon isn't shown on the left and there's content in the default slot) is a thoughtful improvement to the button's appearance.

---

`128-130`: **Good addition of left margin class!**

Adding the `.set-left-margin` class is a nice complement to the existing right margin class, creating a consistent approach to margin handling. The 0.75rem value maintains consistency with the right margin styling as well.

---

`132-134`: **Helpful ghost button padding adjustment!**

This adjustment to remove right padding on ghost buttons helps resolve the padding issues mentioned in the PR objectives. It's a small but important change that will improve the visual consistency of the button component.

</details>
<details>
<summary>src/components/Form/MucSelect.vue (9)</summary>

`31-32`: **Great job importing and setting up Choices.js!**  
This addition will definitely improve the user experience with enhanced select functionality.

---

`34-37`: **Excellent use of refs for DOM and instance tracking.**  
Kindly keep an eye on SSR contexts if the component is ever rendered server-side, since direct DOM references are usually not supported there without conditionals.

---

`96-105`: **Validate object properties before pushing to `displayedItems`.**  
In case an object lacks the specified `itemTitle` key, this approach may cause unexpected results. As a safeguard, consider a fallback label for invalid data.

---

`125-140`: **Configuration is clear and accessible.**  
Thanks for setting `allowHTML: true` to ensure the correct rendering of HTML in options. Keep in mind that it might introduce the possibility of unintended markup, so be sure to sanitize any user-generated input items if needed.

---

`142-156`: **Nice logic in `addChoices()`.**  
Refilling choices with an index-based mechanism is clean. Just ensure that indexes remain valid and that they align safely with `items`.

---

`158-169`: **Mind index bounds in `updateSelectedValues()`.**  
If the user manipulates the DOM or if external data is out of sync, `parseInt(item)` might exceed the array length. A quick guard check will prevent unexpected runtime errors.

---

`171-176`: **Great use of `setDefaultSelectedValue()` within a watcher.**  
This ensures reactive updates to the select whenever the model value changes externally. The structure is quite user-friendly.

---

`214-219`: **onMounted initialization is straightforward.**  
Thank you for wrapping the Choices setup inside the lifecycle hook. This ensures that DOM references are ready before usage.

---

`220-224`: **Clean teardown on unmount.**  
Destroying the instance properly prevents memory leaks and leftover event listeners. Nicely done!

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: CHANGES_REQUESTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-04T09:32:16Z
user: login: coderabbitai[bot]
state: DISMISSED
author_association: CONTRIBUTOR
submitted_at: 2025-03-05T06:38:54Z
user: login: FabianWilms
body: Rest is looking good 👍 
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-05T07:14:57Z
user: login: lehju
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-05T08:13:55Z
user: login: lehju
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-05T08:14:20Z
user: login: coderabbitai[bot]
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-03-05T11:59:02Z
user: login: FabianWilms
state: APPROVED
author_association: MEMBER
submitted_at: 2025-03-05T11:59:11Z

Review comments:
diff_hunk: @@ -151,199 +86,140 @@ const {
    * Property that contains the value to be displayed in the list when a list of objects is used
    */
   itemTitle?: string;
-}>();
 
-/**
- * Toggles the list of items and sets the previously selected item as active
- */
-const toggleItemList = () => {
-  showItems.value = !showItems.value;
-  activeItem.value = lastClickedItem.value;
-};
-
-/**
- * Opens the list of items and sets the previously selected item as active
- */
-const openItemList = () => {
-  showItems.value = true;
-  activeItem.value = lastClickedItem.value;
-  searchValue.value = "";
-};
+  /**
+   * Optional placeholder shown on the input
+   */
+  placeholder?: string;
+}>();
 
-/**
- * Closes the list after clicking outside the component
- */
-useOnClickOutside(selectComponent, () => {
-  showItems.value = false;
-  searchValue.value = outputTransformed.value;
+const computedItems = computed(() => {
+  let displayedItems: string[] = [];
+  items.forEach((item) => {
+    if (typeof item === "string") {
+      displayedItems.push(item);
+    } else {
+      displayedItems.push(item[itemTitle]);
+    }
+  });
+  return displayedItems;
 });
 
-/**
- * Actions upon clicking an item
- * @param clickedValue clicked item value
- */
-const clicked = (clickedValue: MucSelectItemTypes) => {
-  lastClickedItem.value = clickedValue;
-
-  multiple ? updateMVMultiple(clickedValue) : updateMVSingle(clickedValue);
+watch(
+  () => [
+    computedItems.value,
+    multiple,
+    noItemFoundMessage,
+    itemTitle,
+    placeholder,
+  ],
+  () => {
+    if (choicesInstance.value) {
+      choicesInstance.value.destroy();
+      createChoicesInstance();
+      elementRef.value;
+    }
+  }
path: src/components/Form/MucSelect.vue
user: login: coderabbitai[bot]
body: _🛠️ Refactor suggestion_

**Watchers re-initialize Choices on every relevant prop change.**  
This is a friendly reminder that if `items` grows large, consistently destroying and re-instantiating the Choices instance could affect performance. As an optimization, you might update the existing instance’s config or choices wherever possible, rather than recreating it.

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-03-04T09:32:15Z
author_association: CONTRIBUTOR
diff_hunk: @@ -151,199 +86,157 @@ const {
    * Property that contains the value to be displayed in the list when a list of objects is used
    */
   itemTitle?: string;
-}>();
-
-/**
- * Toggles the list of items and sets the previously selected item as active
- */
-const toggleItemList = () => {
-  showItems.value = !showItems.value;
-  activeItem.value = lastClickedItem.value;
-};
 
-/**
- * Opens the list of items and sets the previously selected item as active
- */
-const openItemList = () => {
-  showItems.value = true;
-  activeItem.value = lastClickedItem.value;
-  searchValue.value = "";
-};
+  /**
+   * Optional placeholder shown on the input
+   */
+  placeholder?: string;
+}>();
 
 /**
- * Closes the list after clicking outside the component
+ * Creates an array with the displayed labels.
  */
-useOnClickOutside(selectComponent, () => {
-  showItems.value = false;
-  searchValue.value = outputTransformed.value;
+const displayedLabels = computed(() => {
+  let displayedItems: string[] = [];
+  items.forEach((item) => {
+    if (typeof item === "string") {
+      displayedItems.push(item);
+    } else {
+      displayedItems.push(item[itemTitle]);
+    }
+  });
+  return displayedItems;
 });
 
-/**
- * Actions upon clicking an item
- * @param clickedValue clicked item value
- */
-const clicked = (clickedValue: MucSelectItemTypes) => {
-  lastClickedItem.value = clickedValue;
-
-  multiple ? updateMVMultiple(clickedValue) : updateMVSingle(clickedValue);
+watch(
+  () => [
+    displayedLabels.value,
+    multiple,
+    noItemFoundMessage,
+    itemTitle,
+    placeholder,
+  ],
+  () => {
+    if (choicesInstance.value) {
+      choicesInstance.value.destroy();
+      createChoicesInstance();
+      elementRef.value;
path: src/components/Form/MucSelect.vue
user: login: FabianWilms
body: Why do we need this line?
created_at: 2025-03-05T07:12:11Z
author_association: MEMBER
diff_hunk: @@ -151,199 +86,157 @@ const {
    * Property that contains the value to be displayed in the list when a list of objects is used
    */
   itemTitle?: string;
-}>();
-
-/**
- * Toggles the list of items and sets the previously selected item as active
- */
-const toggleItemList = () => {
-  showItems.value = !showItems.value;
-  activeItem.value = lastClickedItem.value;
-};
 
-/**
- * Opens the list of items and sets the previously selected item as active
- */
-const openItemList = () => {
-  showItems.value = true;
-  activeItem.value = lastClickedItem.value;
-  searchValue.value = "";
-};
+  /**
+   * Optional placeholder shown on the input
+   */
+  placeholder?: string;
+}>();
 
 /**
- * Closes the list after clicking outside the component
+ * Creates an array with the displayed labels.
  */
-useOnClickOutside(selectComponent, () => {
-  showItems.value = false;
-  searchValue.value = outputTransformed.value;
+const displayedLabels = computed(() => {
+  let displayedItems: string[] = [];
+  items.forEach((item) => {
+    if (typeof item === "string") {
+      displayedItems.push(item);
+    } else {
+      displayedItems.push(item[itemTitle]);
+    }
+  });
+  return displayedItems;
 });
 
-/**
- * Actions upon clicking an item
- * @param clickedValue clicked item value
- */
-const clicked = (clickedValue: MucSelectItemTypes) => {
-  lastClickedItem.value = clickedValue;
-
-  multiple ? updateMVMultiple(clickedValue) : updateMVSingle(clickedValue);
+watch(
+  () => [
+    displayedLabels.value,
+    multiple,
+    noItemFoundMessage,
+    itemTitle,
+    placeholder,
+  ],
+  () => {
+    if (choicesInstance.value) {
+      choicesInstance.value.destroy();
+      createChoicesInstance();
+      elementRef.value;
+    }
+  }
+);
 
-  if (!multiple) showItems.value = false;
+/**
+ * Creates a new instance of Choices in which the current configuration is set.
+ * Must be executed for every change, as some of the values cannot be changed on the existing instance.
+ */
+const createChoicesInstance = () => {
+  choicesInstance.value = new Choices(elementRef.value, {
+    allowHTML: true,
+    itemSelectText: "",
+    noResultsText: noItemFoundMessage,
+    placeholder: !!placeholder,
+    placeholderValue: placeholder,
+    removeItemButton: multiple,
+    searchFields: ["label"],
+  });
+  addChoices();
+  setDefaultSelectedValue();
+  choicesInstance.value.passedElement.element.addEventListener(
+    "change",
+    updateSelectedValues
+  );
 };
 
 /**
- * Update the modelValue with the given. Performs conversion to string if necessary.
- * @param newValue the new value
- */
-const updateMVSingle = (newValue: MucSelectItemTypes) => {
-  if (Array.isArray(selectedValues.value))
-    selectedValues.value = selectedValues.value.join(" ");
-
-  if (
-    typeof selectedValues.value !== "string" &&
-    typeof newValue !== "string"
-  ) {
-    selectedValues.value =
-      selectedValues.value[itemTitle] === newValue[itemTitle] ? "" : newValue;
-  } else {
-    selectedValues.value = selectedValues.value === newValue ? "" : newValue;
+ * Replaces the selection options in the existing instance of Choices.
+ */
+const addChoices = () => {
+  if (choicesInstance.value) {
+    choicesInstance.value.clearChoices();
+    let newChoices: ChoiceType[] = [];
+    displayedLabels.value.forEach((item, index) => {
+      newChoices.push({
+        value: index.toString(),
+        label: item,
+        selected: false,
+        disabled: false,
+      });
+    });
+    choicesInstance.value.setChoices(newChoices);
   }
 };
 
 /**
- * Update the modelValue with the given. Performs conversion to array if necessary.
- * @param newValue the new value
+ * Updates the modelValue when the selected option changes.
  */
-const updateMVMultiple = (newValue: MucSelectItemTypes) => {
-  if (!Array.isArray(selectedValues.value))
-    selectedValues.value = [selectedValues.value];
-
-  if (Array.isArray(selectedValues.value)) {
-    if (typeof newValue === "string") {
-      selectedValues.value = selectedValues.value
-        .map((item: string) => item)
-        .includes(newValue)
-        ? selectedValues.value.filter((val: string) => val !== newValue)
-        : [...selectedValues.value, newValue];
+const updateSelectedValues = () => {
+  if (choicesInstance.value) {
+    let values = choicesInstance.value.getValue(true);
+    if (Array.isArray(values)) {
+      let selectedItems: MucSelectItemTypes[] = [];
+      values.forEach((item) => selectedItems.push(items[parseInt(item)]));
+      selectedValues.value = selectedItems;
     } else {
-      selectedValues.value = selectedValues.value
-        .map((item: ItemAsObject) => item[itemTitle])
-        .includes(newValue[itemTitle])
-        ? selectedValues.value.filter(
-            (val: ItemAsObject) => val[itemTitle] !== newValue[itemTitle]
-          )
-        : [...selectedValues.value, newValue];
+      selectedValues.value = items[parseInt(values)];
     }
   }
 };
 
-/**
- * Converts the displayed text depending on the selection mode
- */
-const outputTransformed = computed(() => {
-  if (typeof selectedValues.value === "string") {
-    return selectedValues.value;
-  } else if (!Array.isArray(selectedValues.value)) {
-    return selectedValues.value[itemTitle].toString();
-  } else if (
-    selectedValues.value.every((item: any) => typeof item === "string")
-  ) {
-    return selectedValues.value.join(multiple ? ", " : " ");
-  } else {
-    return selectedValues.value
-      .map((item) => item[itemTitle].toString())
-      .join(multiple ? ", " : " ");
+watch(
+  () => selectedValues.value,
+  () => {
+    setDefaultSelectedValue();
   }
-});
-
-watch(outputTransformed, (newOutput) => {
-  searchValue.value = newOutput;
-});
-
-/**
- * Current search value
- */
-const searchValue = ref<string>(outputTransformed.value);
-
-/**
- * Determines whether all or only the searched elements are displayed
- */
-const displayedItems = computed(() =>
-  searchValue.value == outputTransformed.value
-    ? items
-    : updateDisplayedItems(searchValue.value)
 );
 
 /**
- * Filters the list of elements after entering the search string
- * @param search the search string
- * @return list of searched items
- */
-const updateDisplayedItems = (search: string) => {
-  noItemsFound.value = false;
-  const filteredItems = items.every((item: any) => typeof item === "string")
-    ? items.filter((item) => item.toLowerCase().includes(search.toLowerCase()))
-    : items.filter((item: any) =>
-        item[itemTitle].toString().toLowerCase().includes(search.toLowerCase())
-      );
-  if (filteredItems.length === 0) {
-    noItemsFound.value = true;
+ * Sets the preselected option in the current instance of Choices.
+ */
+const setDefaultSelectedValue = () => {
+  if (choicesInstance.value) {
+    if (selectedValues.value && selectedValues.value.length != 0) {
+      if (Array.isArray(selectedValues.value)) {
+        let selectedItems: string[] = [];
+        selectedValues.value.forEach((value) =>
+          selectedItems.push(
+            items
+              .findIndex((item) => {
+                if (typeof item === "string") {
+                  return item == value;
+                } else {
+                  return item[itemTitle] == value[itemTitle];
+                }
+              })
+              .toString()
+          )
+        );
+        choicesInstance.value.setChoiceByValue(selectedItems);
+      } else {
+        const itemId = items.findIndex((item) => {
+          if (typeof item === "string") {
+            return item == selectedValues.value;
+          } else {
+            const value = selectedValues.value as ItemAsObject;
+            return item[itemTitle] == value[itemTitle];
+          }
+        });
+        choicesInstance.value.setChoiceByValue(itemId.toString());
+      }
+    } else {
+      choicesInstance.value.setChoiceByValue("");
+    }
   }
-  return filteredItems;
 };
 
-/**
- * Apply active class to hovered item
- * @param value of item
- */
-const isActiveClass = (value: MucSelectItemTypes) =>
-  value === activeItem.value ? "active" : "";
-
-/**
- * Apply selected class to selected items
- * @param value of item
- */
-const isSelectedClass = (value: MucSelectItemTypes) => {
-  if (typeof value === "string")
-    return selectedValues.value.includes(value) ? "selected" : "";
-
-  if (Array.isArray(selectedValues.value)) {
-    return selectedValues.value
-      .map((item) => item[itemTitle])
-      .includes(value[itemTitle])
-      ? "selected"
-      : "";
+onMounted(() => {
+  if (elementRef.value) {
path: src/components/Form/MucSelect.vue
user: login: FabianWilms
body: Shouldn't this be

```suggestion
  if (choicesInstance.value) {
```
created_at: 2025-03-05T07:14:40Z
author_association: MEMBER
diff_hunk: @@ -151,199 +86,157 @@ const {
    * Property that contains the value to be displayed in the list when a list of objects is used
    */
   itemTitle?: string;
-}>();
-
-/**
- * Toggles the list of items and sets the previously selected item as active
- */
-const toggleItemList = () => {
-  showItems.value = !showItems.value;
-  activeItem.value = lastClickedItem.value;
-};
 
-/**
- * Opens the list of items and sets the previously selected item as active
- */
-const openItemList = () => {
-  showItems.value = true;
-  activeItem.value = lastClickedItem.value;
-  searchValue.value = "";
-};
+  /**
+   * Optional placeholder shown on the input
+   */
+  placeholder?: string;
+}>();
 
 /**
- * Closes the list after clicking outside the component
+ * Creates an array with the displayed labels.
  */
-useOnClickOutside(selectComponent, () => {
-  showItems.value = false;
-  searchValue.value = outputTransformed.value;
+const displayedLabels = computed(() => {
+  let displayedItems: string[] = [];
+  items.forEach((item) => {
+    if (typeof item === "string") {
+      displayedItems.push(item);
+    } else {
+      displayedItems.push(item[itemTitle]);
+    }
+  });
+  return displayedItems;
 });
 
-/**
- * Actions upon clicking an item
- * @param clickedValue clicked item value
- */
-const clicked = (clickedValue: MucSelectItemTypes) => {
-  lastClickedItem.value = clickedValue;
-
-  multiple ? updateMVMultiple(clickedValue) : updateMVSingle(clickedValue);
+watch(
+  () => [
+    displayedLabels.value,
+    multiple,
+    noItemFoundMessage,
+    itemTitle,
+    placeholder,
+  ],
+  () => {
+    if (choicesInstance.value) {
+      choicesInstance.value.destroy();
+      createChoicesInstance();
+      elementRef.value;
path: src/components/Form/MucSelect.vue
user: login: lehju
body: Done.
created_at: 2025-03-05T08:13:55Z
author_association: CONTRIBUTOR
diff_hunk: @@ -151,199 +86,157 @@ const {
    * Property that contains the value to be displayed in the list when a list of objects is used
    */
   itemTitle?: string;
-}>();
-
-/**
- * Toggles the list of items and sets the previously selected item as active
- */
-const toggleItemList = () => {
-  showItems.value = !showItems.value;
-  activeItem.value = lastClickedItem.value;
-};
 
-/**
- * Opens the list of items and sets the previously selected item as active
- */
-const openItemList = () => {
-  showItems.value = true;
-  activeItem.value = lastClickedItem.value;
-  searchValue.value = "";
-};
+  /**
+   * Optional placeholder shown on the input
+   */
+  placeholder?: string;
+}>();
 
 /**
- * Closes the list after clicking outside the component
+ * Creates an array with the displayed labels.
  */
-useOnClickOutside(selectComponent, () => {
-  showItems.value = false;
-  searchValue.value = outputTransformed.value;
+const displayedLabels = computed(() => {
+  let displayedItems: string[] = [];
+  items.forEach((item) => {
+    if (typeof item === "string") {
+      displayedItems.push(item);
+    } else {
+      displayedItems.push(item[itemTitle]);
+    }
+  });
+  return displayedItems;
 });
 
-/**
- * Actions upon clicking an item
- * @param clickedValue clicked item value
- */
-const clicked = (clickedValue: MucSelectItemTypes) => {
-  lastClickedItem.value = clickedValue;
-
-  multiple ? updateMVMultiple(clickedValue) : updateMVSingle(clickedValue);
+watch(
+  () => [
+    displayedLabels.value,
+    multiple,
+    noItemFoundMessage,
+    itemTitle,
+    placeholder,
+  ],
+  () => {
+    if (choicesInstance.value) {
+      choicesInstance.value.destroy();
+      createChoicesInstance();
+      elementRef.value;
+    }
+  }
+);
 
-  if (!multiple) showItems.value = false;
+/**
+ * Creates a new instance of Choices in which the current configuration is set.
+ * Must be executed for every change, as some of the values cannot be changed on the existing instance.
+ */
+const createChoicesInstance = () => {
+  choicesInstance.value = new Choices(elementRef.value, {
+    allowHTML: true,
+    itemSelectText: "",
+    noResultsText: noItemFoundMessage,
+    placeholder: !!placeholder,
+    placeholderValue: placeholder,
+    removeItemButton: multiple,
+    searchFields: ["label"],
+  });
+  addChoices();
+  setDefaultSelectedValue();
+  choicesInstance.value.passedElement.element.addEventListener(
+    "change",
+    updateSelectedValues
+  );
 };
 
 /**
- * Update the modelValue with the given. Performs conversion to string if necessary.
- * @param newValue the new value
- */
-const updateMVSingle = (newValue: MucSelectItemTypes) => {
-  if (Array.isArray(selectedValues.value))
-    selectedValues.value = selectedValues.value.join(" ");
-
-  if (
-    typeof selectedValues.value !== "string" &&
-    typeof newValue !== "string"
-  ) {
-    selectedValues.value =
-      selectedValues.value[itemTitle] === newValue[itemTitle] ? "" : newValue;
-  } else {
-    selectedValues.value = selectedValues.value === newValue ? "" : newValue;
+ * Replaces the selection options in the existing instance of Choices.
+ */
+const addChoices = () => {
+  if (choicesInstance.value) {
+    choicesInstance.value.clearChoices();
+    let newChoices: ChoiceType[] = [];
+    displayedLabels.value.forEach((item, index) => {
+      newChoices.push({
+        value: index.toString(),
+        label: item,
+        selected: false,
+        disabled: false,
+      });
+    });
+    choicesInstance.value.setChoices(newChoices);
   }
 };
 
 /**
- * Update the modelValue with the given. Performs conversion to array if necessary.
- * @param newValue the new value
+ * Updates the modelValue when the selected option changes.
  */
-const updateMVMultiple = (newValue: MucSelectItemTypes) => {
-  if (!Array.isArray(selectedValues.value))
-    selectedValues.value = [selectedValues.value];
-
-  if (Array.isArray(selectedValues.value)) {
-    if (typeof newValue === "string") {
-      selectedValues.value = selectedValues.value
-        .map((item: string) => item)
-        .includes(newValue)
-        ? selectedValues.value.filter((val: string) => val !== newValue)
-        : [...selectedValues.value, newValue];
+const updateSelectedValues = () => {
+  if (choicesInstance.value) {
+    let values = choicesInstance.value.getValue(true);
+    if (Array.isArray(values)) {
+      let selectedItems: MucSelectItemTypes[] = [];
+      values.forEach((item) => selectedItems.push(items[parseInt(item)]));
+      selectedValues.value = selectedItems;
     } else {
-      selectedValues.value = selectedValues.value
-        .map((item: ItemAsObject) => item[itemTitle])
-        .includes(newValue[itemTitle])
-        ? selectedValues.value.filter(
-            (val: ItemAsObject) => val[itemTitle] !== newValue[itemTitle]
-          )
-        : [...selectedValues.value, newValue];
+      selectedValues.value = items[parseInt(values)];
     }
   }
 };
 
-/**
- * Converts the displayed text depending on the selection mode
- */
-const outputTransformed = computed(() => {
-  if (typeof selectedValues.value === "string") {
-    return selectedValues.value;
-  } else if (!Array.isArray(selectedValues.value)) {
-    return selectedValues.value[itemTitle].toString();
-  } else if (
-    selectedValues.value.every((item: any) => typeof item === "string")
-  ) {
-    return selectedValues.value.join(multiple ? ", " : " ");
-  } else {
-    return selectedValues.value
-      .map((item) => item[itemTitle].toString())
-      .join(multiple ? ", " : " ");
+watch(
+  () => selectedValues.value,
+  () => {
+    setDefaultSelectedValue();
   }
-});
-
-watch(outputTransformed, (newOutput) => {
-  searchValue.value = newOutput;
-});
-
-/**
- * Current search value
- */
-const searchValue = ref<string>(outputTransformed.value);
-
-/**
- * Determines whether all or only the searched elements are displayed
- */
-const displayedItems = computed(() =>
-  searchValue.value == outputTransformed.value
-    ? items
-    : updateDisplayedItems(searchValue.value)
 );
 
 /**
- * Filters the list of elements after entering the search string
- * @param search the search string
- * @return list of searched items
- */
-const updateDisplayedItems = (search: string) => {
-  noItemsFound.value = false;
-  const filteredItems = items.every((item: any) => typeof item === "string")
-    ? items.filter((item) => item.toLowerCase().includes(search.toLowerCase()))
-    : items.filter((item: any) =>
-        item[itemTitle].toString().toLowerCase().includes(search.toLowerCase())
-      );
-  if (filteredItems.length === 0) {
-    noItemsFound.value = true;
+ * Sets the preselected option in the current instance of Choices.
+ */
+const setDefaultSelectedValue = () => {
+  if (choicesInstance.value) {
+    if (selectedValues.value && selectedValues.value.length != 0) {
+      if (Array.isArray(selectedValues.value)) {
+        let selectedItems: string[] = [];
+        selectedValues.value.forEach((value) =>
+          selectedItems.push(
+            items
+              .findIndex((item) => {
+                if (typeof item === "string") {
+                  return item == value;
+                } else {
+                  return item[itemTitle] == value[itemTitle];
+                }
+              })
+              .toString()
+          )
+        );
+        choicesInstance.value.setChoiceByValue(selectedItems);
+      } else {
+        const itemId = items.findIndex((item) => {
+          if (typeof item === "string") {
+            return item == selectedValues.value;
+          } else {
+            const value = selectedValues.value as ItemAsObject;
+            return item[itemTitle] == value[itemTitle];
+          }
+        });
+        choicesInstance.value.setChoiceByValue(itemId.toString());
+      }
+    } else {
+      choicesInstance.value.setChoiceByValue("");
+    }
   }
-  return filteredItems;
 };
 
-/**
- * Apply active class to hovered item
- * @param value of item
- */
-const isActiveClass = (value: MucSelectItemTypes) =>
-  value === activeItem.value ? "active" : "";
-
-/**
- * Apply selected class to selected items
- * @param value of item
- */
-const isSelectedClass = (value: MucSelectItemTypes) => {
-  if (typeof value === "string")
-    return selectedValues.value.includes(value) ? "selected" : "";
-
-  if (Array.isArray(selectedValues.value)) {
-    return selectedValues.value
-      .map((item) => item[itemTitle])
-      .includes(value[itemTitle])
-      ? "selected"
-      : "";
+onMounted(() => {
+  if (elementRef.value) {
path: src/components/Form/MucSelect.vue
user: login: lehju
body: choicesInstance.value cannot yet exist at this point, as it is only initialized afterwards.
This checks whether the select element exists.
created_at: 2025-03-05T08:14:20Z
author_association: CONTRIBUTOR

PR comments:
user: login: lehju
created_at: 2025-03-04T09:29:23Z
body: @coderabbitai review
author_association: CONTRIBUTOR
user: login: coderabbitai[bot]
created_at: 2025-03-04T09:32:12Z
body: <!-- This is an auto-generated comment: summarize by coderabbit.ai -->
<!-- walkthrough_start -->

## Walkthrough
This pull request adds the Choices.js dependency to enhance select inputs and updates several components accordingly. The button component's computed classes are adjusted for improved icon margin handling. The MucSelect functionality is expanded via a refactor that replaces a custom dropdown with a native `<select>` element managed by Choices.js, along with updated exports and type definitions. Additionally, an obsolete select item component has been removed.

## Changes

| Files                                                                                               | Summary                                                                                                                                                                                                                                                                                                                   |
|-----------------------------------------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `package.json`                                                                                      | Added dependency `"choices.js": "11.0.3"`.                                                                                                                                                                                                                                                                                |
| `src/components/Button/MucButton.vue`                                                               | Updated computed properties to adjust icon positioning and margins; added new CSS classes `.set-left-margin` (for left margin) and `.m-button--ghost` (for no right padding).                                                                                                                                       |
| `src/components/Form/MucSelect.(stories.ts, vue, Item.vue, Types.ts)`                                 | In `MucSelect.stories.ts`, updated the `MultiSelect` export with a new `modelValue` array and added a new `Placeholder` export; in `MucSelect.vue`, refactored the component to use a native `<select>` element enhanced by Choices.js (with new variables and methods such as `createChoicesInstance`, `addChoices`, and `updateSelectedValues`); removed `MucSelectItem.vue`; added new type `ChoiceType` in `MucSelectTypes.ts`.  |

## Sequence Diagram(s)
```mermaid
sequenceDiagram
    participant M as MucSelect Component
    participant C as Choices.js Instance
    Note over M,C: Component Initialization
    M->>C: createChoicesInstance()
    C-->>M: Instance ready
    M->>C: addChoices() to load options
    Note over M: User selects option
    M->>C: updateSelectedValues()
    C-->>M: Selection updated
    Note over M,C: Component Unmount
    M->>C: destroy()
```

## Possibly related PRs
- `it-at-m/muc-patternlab-vue#379`: Addresses similar modifications in the MucSelect component for enhanced hint display functionality.

## Suggested labels
`enhancement`, `bug`, `released`, `released on @beta`

## Suggested reviewers
- FabianWilms
- langehm

## Poem
> In lines of code our journey unfurls,  
> Choices awaken the select in our worlds.  
> Button margins shifted with a graceful tweak,  
> New types align and old files now speak.  
> Cheers to our code—bright and sleek!  
>  
> 🚀 Happy coding and smooth sailing!

<!-- walkthrough_end -->
<!-- internal state start -->


<!-- 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 -->

<!-- internal state end -->
<!-- finishing_touch_checkbox_start -->

<details>
<summary>✨ Finishing Touches</summary>

- [ ] <!-- {"checkboxId": "7962f53c-55bc-4827-bfbf-6a18da830691"} --> 📝 Generate Docstrings

</details>

<!-- finishing_touch_checkbox_end -->
<!-- tips_start -->

---



<details>
<summary>🪧 Tips</summary>

### Chat

There are 3 ways to chat with [CodeRabbit](https://coderabbit.ai):

- Review comments: Directly reply to a review comment made by CodeRabbit. Example:
  - `I pushed a fix in commit <commit_id>, please review it.`
  - `Generate unit testing code for this file.`
  - `Open a follow-up GitHub issue for this discussion.`
- Files and specific lines of code (under the "Files changed" tab): Tag `@coderabbitai` in a new review comment at the desired location with your query. Examples:
  - `@coderabbitai generate unit testing code for this file.`
  -	`@coderabbitai modularize this function.`
- PR comments: Tag `@coderabbitai` in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
  - `@coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.`
  - `@coderabbitai read src/utils.ts and generate unit testing code.`
  - `@coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.`
  - `@coderabbitai help me debug CodeRabbit configuration file.`

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

### CodeRabbit Commands (Invoked using PR comments)

- `@coderabbitai pause` to pause the reviews on a PR.
- `@coderabbitai resume` to resume the paused reviews.
- `@coderabbitai review` to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
- `@coderabbitai full review` to do a full review from scratch and review all the files again.
- `@coderabbitai summary` to regenerate the summary of the PR.
- `@coderabbitai generate docstrings` to [generate docstrings](https://docs.coderabbit.ai/finishing-touches/docstrings) for this PR.
- `@coderabbitai resolve` resolve all the CodeRabbit review comments.
- `@coderabbitai configuration` to show the current CodeRabbit configuration for the repository.
- `@coderabbitai help` to get help.

### Other keywords and placeholders

- Add `@coderabbitai ignore` anywhere in the PR description to prevent this PR from being reviewed.
- Add `@coderabbitai summary` to generate the high-level summary at a specific location in the PR description.
- Add `@coderabbitai` anywhere in the PR title to generate the title automatically.

### Documentation and Community

- Visit our [Documentation](https://docs.coderabbit.ai) for detailed information on how to use CodeRabbit.
- Join our [Discord Community](http://discord.gg/coderabbit) to get help, request features, and share feedback.
- Follow us on [X/Twitter](https://twitter.com/coderabbitai) for updates and announcements.

</details>

<!-- tips_end -->
author_association: CONTRIBUTOR
user: login: github-actions[bot]
created_at: 2025-03-05T12:14:32Z
body: :tada: This PR is included in version 4.0.0-beta.2 :tada:

The release is available on:
- [npm package (@beta dist-tag)](https://www.npmjs.com/package/@muenchen/muc-patternlab-vue/v/4.0.0-beta.2)
- [GitHub release](https://github.com/it-at-m/muc-patternlab-vue/releases/tag/v4.0.0-beta.2)

Your **[semantic-release](https://github.com/semantic-release/semantic-release)** bot :package::rocket:
author_association: NONE
user: login: github-actions[bot]
created_at: 2025-03-11T12:34:23Z
body: :tada: This PR is included in version 4.0.0 :tada:

The release is available on:
- [npm package (@latest dist-tag)](https://www.npmjs.com/package/@muenchen/muc-patternlab-vue/v/4.0.0)
- [GitHub release](https://github.com/it-at-m/muc-patternlab-vue/releases/tag/v4.0.0)

Your **[semantic-release](https://github.com/semantic-release/semantic-release)** bot :package::rocket:
author_association: NONE