Elements

Meta Selects

29 Jul 2023

HTML
SCSS
PostCSS
JS
                            <div class="meta-select">
  <label for="lorem-one">Lorem Ipsum</label>
  <select class="choices-select" id="lorem-one" data-choices>
    <option value="">Select</option>

    <option value="Lorem-1">Lorem-1</option>
    <option value="Lorem-2">Lorem-2</option>
    <option value="Lorem-3">Lorem-3</option>
    <option value="Lorem-4">Lorem-4</option>
  </select>
</div>
<div class="meta-select wide-type full-type">
  <label for="lorem-multiple">Lorem Ipsum Multiple</label>
  <select
    class="choices-select"
    id="lorem-multiple"
    data-choices
    multiple
    data-multiple-list-logic
  >
    <option value="">Select</option>

    <option value="Lorem-1">Lorem-1</option>
    <option value="Lorem-2">Lorem-2</option>
    <option value="Lorem-3">Lorem-3</option>
    <option value="Lorem-4">Lorem-4</option>
  </select>
</div>
                        
                            .meta-select {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  justify-content: flex-start;

  label {
    font-family: var(--font-main);
    font-size: ac(16px, 14px);
    line-height: 1.2;
    font-weight: 700;
    color: var(--cl-dark-blue);
    padding-bottom: 8px;
    cursor: pointer;
  }

  select {
    opacity: 0;
    height: ac(50px, 48px);
  }

  .choices {
    width: 100%;
    margin-bottom: 0;
    overflow: visible;

    &__inner {
      border: 2px solid var(--cl-grey);
      border-radius: 6px;
      outline: none;
      transition: box-shadow 0.3s ease, border-color 0.3s ease,
        border-radius 0.3s ease, caret-color 0.3s ease, color 0.3s ease;
      padding: 10px ac(16px, 12px);
      background: transparent;
      display: flex;
      align-items: center;
      justify-content: flex-start;
      @include max-line-length(1);
      width: 100%;
      cursor: pointer;
      font-size: ac(16px, 14px);
      line-height: 1.2;
      font-weight: 500;
      font-family: var(--font-main);
      color: var(--cl-dark-blue);
      box-shadow: 0 1px 2px 0 rgba(var(--cl-dark-blue-rgb) / 0.05);

      &:hover {
        border-color: var(--cl-dark-blue);
      }
      &.error {
	border-color: var(--cl-red);
	box-shadow: 2px 2px 5px 0px rgba(var(--cl-red-rgb) / 0.3);
      }
    }
    &.is-open {
      &:not(.is-flipped) {
        .choices__inner {
          border-radius: 6px 6px 0 0;
        }

        .choices__list--dropdown,
        .choices__list[aria-expanded] {
          border-top: none;
          margin-top: 0;
          border-radius: 0 0 6px 6px;
        }
      }

      &.is-flipped {
        .choices__inner {
          border-radius: 0 0 6px 6px;
        }

        .choices__list--dropdown,
        .choices__list[aria-expanded] {
          margin-bottom: 0;
          border-bottom: none;
          border-radius: 6px 6px 0 0;
        }
      }
    }

    &__item {
      @include max-line-length(1);
    }

    &__placeholder {
      color: var(--cl-dark-blue);
      opacity: 1;
    }

    &__list {
      padding: 0;
      background-color: var(--cl-white);

      .choices__item {
        padding-right: ac(16px, 12px);
        font-size: ac(16px, 14px);
        line-height: 1.2;
        font-weight: 500;
        font-family: var(--font-main);

        &.is-selected {
          color: var(--cl-primary);
        }
      }

      &--multiple {
        color: var(--cl-dark-blue);
        .choices__item--selectable {
          display: none;
        }

        + .choices__input {
          display: none;
        }
      }

      &--dropdown {
        .choices__item {
          color: var(--cl-dark-blue);
        }
      }
    }

    &[data-type*='select-one'],
    &[data-type*='select-multiple'] {
      cursor: pointer;
      &:after {
        border: none;
        border-bottom: 1px solid var(--cl-dark-blue);
        border-right: 1px solid var(--cl-dark-blue);
        content: '';
        display: block;
        height: 5px;
        margin-top: -4px;
        pointer-events: none;
        position: absolute;
        right: ac(25px, 18px);
        top: 50%;
        transform-origin: 66% 66%;
        transform: rotate(45deg) scale(1.5);
        transition: all 0.15s ease-in-out;
        width: 5px;
      }
    }

    &.is-open {
      &:after {
        transform: rotate(-135deg) scale(1.5);
      }
    }

    &__list--dropdown .choices__item--selectable.is-highlighted,
    &__list[aria-expanded] .choices__item--selectable.is-highlighted {
      background-color: transparent;

      &:not(.is-selected) {
        color: var(--cl-dark-grey);
      }
    }

    &__list--dropdown,
    .choices__list[aria-expanded] {
      border: 2px solid var(--cl-grey);
      z-index: 20;
      border-radius: 6px;
    }

    &[data-type*='select-one'] .choices__input {
      margin: 2px;
      max-width: calc(100% - 4px);
      border: 2px solid var(--cl-grey);
      border-radius: 8px;
      background: var(--transparent);
      color: var(--cl-dark-grey);
      font-size: ac(16px, 14px);
      line-height: 1.2;
      font-weight: 500;
      font-family: var(--font-main);
    }

    &[data-type*='select-multiple'] .choices__inner {
      cursor: pointer;
    }
  }

  &__list {
    display: none;
    align-items: center;
    justify-content: flex-start;
    flex-wrap: wrap;
    max-width: 100%;
    grid-column-gap: 10px;
    grid-row-gap: 8px;

    &.is-visible {
      margin-top: 10px;
      display: flex;
    }
  }

  &__list-item {
    position: relative;
    font-size: ac(16px, 14px);
    line-height: 1.2;
    font-weight: 500;
    font-family: var(--font-main);
    padding-right: 14px;
    cursor: default;
    color: var(--cl-dark-blue);

    &:after {
      content: '';
      cursor: pointer;
      display: block;
      position: absolute;
      top: 50%;
      right: 0;
      transform: translateY(-50%);
      width: 10px;
      height: 10px;
      transition: background-color 0.25s ease, opacity 0.25s ease,
        transform 0.25s ease;
      opacity: 0.5;
      background: var(--cl-dark-blue);
      clip-path: polygon(
        20% 0%,
        0% 20%,
        30% 50%,
        0% 80%,
        20% 100%,
        50% 70%,
        80% 100%,
        100% 80%,
        70% 50%,
        100% 20%,
        80% 0%,
        50% 30%
      );
    }

    &:hover {
      &:after {
        opacity: 1;
        transform: translateY(-50%) scale(1.15);
      }
    }
  }

  &__help,
  &__error {
    margin-top: 5px;
    font-family: var(--font-main);
    font-size: ac(14px, 12px);
    line-height: 1.2;
    font-weight: 600;
  }

  &__help {
    color: var(--cl-dark-grey);
  }

  &__error {
    color: var(--cl-red);
  }
}
                        
                            .meta-select {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  justify-content: flex-start;

  label {
    font-family: var(--font-main);
    font-size: ac(16px, 14px);
    line-height: 1.2;
    font-weight: 700;
    color: var(--cl-dark-blue);
    padding-bottom: 8px;
    cursor: pointer;
  }

  select {
    opacity: 0;
    height: ac(50px, 48px);
  }

  .choices {
    width: 100%;
    margin-bottom: 0;
    overflow: visible;

    &__inner {
      border: 2px solid var(--cl-grey);
      border-radius: 6px;
      outline: none;
      transition: box-shadow 0.3s ease, border-color 0.3s ease,
        border-radius 0.3s ease, caret-color 0.3s ease, color 0.3s ease;
      padding: 10px ac(16px, 12px);
      background: transparent;
      display: flex;
      align-items: center;
      justify-content: flex-start;
      @mixin max-line-length-one;
      width: 100%;
      cursor: pointer;
      font-size: ac(16px, 14px);
      line-height: 1.2;
      font-weight: 500;
      font-family: var(--font-main);
      color: var(--cl-dark-blue);
      box-shadow: 0 1px 2px 0 rgba(var(--cl-dark-blue-rgb) / 0.05);

      &:hover {
        border-color: var(--cl-dark-blue);
      }
      &.error {
	border-color: var(--cl-red);
	box-shadow: 2px 2px 5px 0px rgba(var(--cl-red-rgb) / 0.3);
      }
    }
    &.is-open {
      &:not(.is-flipped) {
        .choices__inner {
          border-radius: 6px 6px 0 0;
        }

        .choices__list--dropdown,
        .choices__list[aria-expanded] {
          border-top: none;
          margin-top: 0;
          border-radius: 0 0 6px 6px;
        }
      }

      &.is-flipped {
        .choices__inner {
          border-radius: 0 0 6px 6px;
        }

        .choices__list--dropdown,
        .choices__list[aria-expanded] {
          margin-bottom: 0;
          border-bottom: none;
          border-radius: 6px 6px 0 0;
        }
      }
    }

    &__item {
      @mixin max-line-length-one;
    }

    &__placeholder {
      color: var(--cl-dark-blue);
      opacity: 1;
    }

    &__list {
      padding: 0;
      background-color: var(--cl-white);

      .choices__item {
        padding-right: ac(16px, 12px);
        font-size: ac(16px, 14px);
        line-height: 1.2;
        font-weight: 500;
        font-family: var(--font-main);

        &.is-selected {
          color: var(--cl-primary);
        }
      }

      &--multiple {
        color: var(--cl-dark-blue);
        .choices__item--selectable {
          display: none;
        }

        + .choices__input {
          display: none;
        }
      }

      &--dropdown {
        .choices__item {
          color: var(--cl-dark-blue);
        }
      }
    }

    &[data-type*='select-one'],
    &[data-type*='select-multiple'] {
      cursor: pointer;
      &:after {
        border: none;
        border-bottom: 1px solid var(--cl-dark-blue);
        border-right: 1px solid var(--cl-dark-blue);
        content: '';
        display: block;
        height: 5px;
        margin-top: -4px;
        pointer-events: none;
        position: absolute;
        right: ac(25px, 18px);
        top: 50%;
        transform-origin: 66% 66%;
        transform: rotate(45deg) scale(1.5);
        transition: all 0.15s ease-in-out;
        width: 5px;
      }
    }

    &.is-open {
      &:after {
        transform: rotate(-135deg) scale(1.5);
      }
    }

    &__list--dropdown .choices__item--selectable.is-highlighted,
    &__list[aria-expanded] .choices__item--selectable.is-highlighted {
      background-color: transparent;

      &:not(.is-selected) {
        color: var(--cl-dark-grey);
      }
    }

    &__list--dropdown,
    .choices__list[aria-expanded] {
      border: 2px solid var(--cl-grey);
      z-index: 20;
      border-radius: 6px;
    }

    &[data-type*='select-one'] .choices__input {
      margin: 2px;
      max-width: calc(100% - 4px);
      border: 2px solid var(--cl-grey);
      border-radius: 8px;
      background: var(--transparent);
      color: var(--cl-dark-grey);
      font-size: ac(16px, 14px);
      line-height: 1.2;
      font-weight: 500;
      font-family: var(--font-main);
    }

    &[data-type*='select-multiple'] .choices__inner {
      cursor: pointer;
    }
  }

  &__list {
    display: none;
    align-items: center;
    justify-content: flex-start;
    flex-wrap: wrap;
    max-width: 100%;
    grid-column-gap: 10px;
    grid-row-gap: 8px;

    &.is-visible {
      margin-top: 10px;
      display: flex;
    }
  }

  &__list-item {
    position: relative;
    font-size: ac(16px, 14px);
    line-height: 1.2;
    font-weight: 500;
    font-family: var(--font-main);
    padding-right: 14px;
    cursor: default;
    color: var(--cl-dark-blue);

    &:after {
      content: '';
      cursor: pointer;
      display: block;
      position: absolute;
      top: 50%;
      right: 0;
      transform: translateY(-50%);
      width: 10px;
      height: 10px;
      transition: background-color 0.25s ease, opacity 0.25s ease,
        transform 0.25s ease;
      opacity: 0.5;
      background: var(--cl-dark-blue);
      clip-path: polygon(
        20% 0%,
        0% 20%,
        30% 50%,
        0% 80%,
        20% 100%,
        50% 70%,
        80% 100%,
        100% 80%,
        70% 50%,
        100% 20%,
        80% 0%,
        50% 30%
      );
    }

    &:hover {
      &:after {
        opacity: 1;
        transform: translateY(-50%) scale(1.15);
      }
    }
  }
  &__help,
  &__error {
    margin-top: 5px;
    font-family: var(--font-main);
    font-size: ac(14px, 12px);
    line-height: 1.2;
    font-weight: 600;
  }

  &__help {
    color: var(--cl-dark-grey);
  }

  &__error {
    color: var(--cl-red);
  }
}
                        
                            const choicesDOM = document.querySelector(['[data-choices]']);
if (choicesDOM) {
  const choicesArr = document.querySelectorAll(['[data-choices]']);
  for (let i = 0; i < choicesArr.length; i++) {
    const parentContainer = choicesArr[i].parentNode;
    /*const list = parentContainer.querySelector(".default-select__list");*/

    const choices = new Choices(choicesArr[i], {
      searchEnabled: true,
      itemSelectText: '',
      placeholder: true,
      searchPlaceholderValue: 'Search',
    });

    const choicesMultipleElement = parentContainer.querySelector(
      ".choices[data-type='select-multiple']"
    );

    if (choicesMultipleElement) {
      choicesMultipleElement.addEventListener('click', () => {
        if (parentContainer.querySelector('.is-open')) {
          choices.hideDropdown();
        }
      });
    }

    /* New multiselect logic */
    if (
      choicesArr[i].multiple &&
      choicesArr[i].hasAttribute('data-multiple-list-logic')
    ) {
      let optionName = null;
      let optionValue = null;

      const multiplePlaceholder = parentContainer.querySelector(
        '.choices__list--multiple'
      );

      const list = document.createElement('ul');
      list.className = 'meta-select__list';
      parentContainer.appendChild(list);

      function createListItem(optionName, optionValue) {
        const listItem = document.createElement('li');
        listItem.setAttribute('data-val', optionValue);
        listItem.innerHTML = `${optionName}`;
        listItem.classList.add('meta-select__list-item');
        list.appendChild(listItem);

        listItem.addEventListener('click', () => {
          handleListItemClick(listItem);
        });
      }
      function handleSelectedOptions() {
        list.innerHTML = '';

        const selectedOptions = Array.from(choicesArr[i].selectedOptions);

        if (selectedOptions.length >= 1) {
          list.classList.add('is-visible');
        } else {
          list.classList.remove('is-visible');
        }

        if (selectedOptions.length === 0 && !choicesArr[i].multiple) {
          choices.setChoiceByValue('');
        }

        selectedOptions.forEach(function (option) {
          optionName = option.textContent;
          optionValue = option.value;
          if (optionName !== 'Select') {
            createListItem(optionName, optionValue);
          }
        });

        const listArr = list.querySelectorAll('.meta-select__list-item');
        if (listArr.length === 1) {
          multiplePlaceholder.textContent = optionName;
        } else if (listArr.length >= 2) {
          multiplePlaceholder.textContent = `Selected ${listArr.length} items`;
        } else {
          multiplePlaceholder.textContent = 'Select';
        }
      }

      function handleListItemClick(listItem) {
        const optionValue = listItem.getAttribute('data-val');

        choices.removeActiveItemsByValue(optionValue);
        handleSelectedOptions();
      }

      handleSelectedOptions();

      choicesArr[i].addEventListener('change', function () {
        handleSelectedOptions();
      });

      list.addEventListener('click', function (event) {
        const liElement = event.target.closest('.meta-select__list-item');
        if (liElement) {
          handleListItemClick(liElement);
        }
      });
    }
  }
}
                        

Будемо намагатися стандартизувати розмітку селектів в
нових проєктах. Викорситовується плагін Choices. В даному прикладі одразу 2 варіанти селектів, стандартний та мультиселект. Краще щоб одразу продумувати обидва варіанти, тому що неодноразово приходили таски, щоб якомусь селекту додати мультивибір. Але реалізація в плагіні не дуже подобається, в прикладі дещо доробив її, буде створюватись список з вибраними опціями, які по кліку можна буде забрати, а в самому плейсхолдері, якщо вибрана одна опція, то буде її назва, якщо декілька, то буде виводитись кількість вибраних. Якщо така логіка не потрібна, то її можна легко забрати не додаючи до селекту атрибут data-multiple-list-logic.

Цей варіант мінімально стилізований.

Якщо в дизайні проєкту не потрібен label, не забувайте додавати клас sr-only, щоб його було не видно. Але він потрібен для SEO та правильної семантики DOM.


Якщо треба додати якусь підсказку під селектом, то для цього в розмітці використовуйте span з класом "meta-select_help". Наприклад:

<div class="meta-select">
<label for="lorem-one">Lorem Ipsum</label>
<select class="choices-select" id="lorem-one" data-choices>
<option value="">Select</option>

<option value="Lorem-1">Lorem-1</option>
<option value="Lorem-2">Lorem-2</option>
<option value="Lorem-3">Lorem-3</option>
<option value="Lorem-4">Lorem-4</option>
</select>
<span class="meta-select__help">Lorem ipsum dolor set.</span>
</div>


Якщо треба помилку вивести, то для цього в розмітці використовуйте span з класом "meta-select__error". Та choices через js передайте класс error Наприклад:

<div class="meta-select">
<label for="lorem-one">Lorem Ipsum</label>
<select class="choices-select" id="lorem-one" data-choices>
<option value="">Select</option>

<option value="Lorem-1">Lorem-1</option>
<option value="Lorem-2">Lorem-2</option>
<option value="Lorem-3">Lorem-3</option>
<option value="Lorem-4">Lorem-4</option>
</select>
<span class="meta-select__error">The choice must be made.</span>
</div>
0