← back to Dw Theme Boost Fix

snippets/element.select.liquid

236 lines

{% doc %}
  Copyright © 2025 Archetype Themes LP. All rights reserved.

  Custom select element that looks like a button with a chevron icon.
  Uses the faux select pattern with a custom element for enhanced functionality.
  Maintains native select behavior while providing custom styling.

  Element
  @param {string} [name] - Name attribute for the select element
  @param {string} [id] - ID attribute for the select element
  @param {string} [placeholder] - Placeholder text when no option is selected
  @param {boolean} [disabled] - Whether the select is disabled
  @param {string} [variant] - Button variant (default, primary, outline)
  @param {string} [size] - Button size (sm, md, lg)
  @param {string} [class] - Additional CSS classes
  @param {string} [attributes] - Additional attributes for the select element

  Content
  @param {string} [slot] - Option elements to render inside the select

  @example
  {% capture options %}
    <option value="">Choose an option</option>
    <option value="option1">Option 1</option>
    <option value="option2">Option 2</option>
  {% endcapture %}
  
  {% render 'element.select',
     name: 'my-select',
     placeholder: 'Select an option',
     variant: 'primary',
     slot: options %}
{% enddoc %}

{% liquid
  # Element Defaults
  assign name = name | default: ''
  assign id = id | default: ''
  assign placeholder = placeholder | default: ''
  assign transparent = transparent | default: false
  assign disabled = disabled | default: false
  assign variant = variant | default: 'default'
  assign size = size | default: 'md'
  assign class = class | default: ''
  assign attributes = attributes | default: ''

  # Build class list
  assign wrapper_classes = 'element-select__wrapper'

  if disabled
    assign wrapper_classes = wrapper_classes | append: ' element-select__wrapper--disabled'
  endif

  if variant != 'default'
    assign button_classes = button_classes | append: ' select-button--' | append: variant
  endif

  if size != 'md'
    assign button_classes = button_classes | append: ' select-button--' | append: size
  endif

  if class != ''
    assign wrapper_classes = wrapper_classes | append: ' ' | append: class
  endif
%}

<element-select class="{{ wrapper_classes }}">
  <select 
    name="{{ name }}"
    id="{{ id }}"
    {% if disabled %}disabled{% endif %}
    aria-label="{{ placeholder }}"
    tabindex="0"
    {{ attributes }}
  >
    {{ slot }}
  </select>
  
  {% render 'element.button', 
     classlist: button_classes, 
     icon: 'chevron-down', 
     icon_position: 'end', 
     inverted: true,
     transparent: transparent,
     text: placeholder,
     attributes: 'tabindex="-1"'
  %}
</element-select>

{% stylesheet %}
  element-select {
    display: inline-block;
    position: relative;
  }

  /* Ensure the custom element inherits the wrapper styles */
  element-select.element-select__wrapper {
    position: relative;
    display: inline-block;
  }

  element-select.element-select__wrapper select {
    position: absolute;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
    opacity: 0;
    cursor: pointer;
    z-index: 2;
    /* Ensure the select is focusable and visible to screen readers */
    pointer-events: auto;
  }

  /* Focus state for the wrapper */
  element-select.element-select__wrapper--focused .element-button {
    --_box-shadow: 0 0 0 var(--element-input-box-shadow-spread-radius--focus) color-mix(in srgb, var(--color-primary) var(--element-input-box-shadow-shade--focus), transparent);
  }

  element-select select:focus-visible + .element-button {
    --_border-width: var(--element-input-box-shadow-spread-radius--focus);
    --_box-shadow: inset 0 0 0 var(--_border-width) var(--_color-focus);
  }

  /* Ensure the button takes full width */
  element-select .element-button {
    width: 100%;
    justify-content: space-between;
  }
{% endstylesheet %}

{% javascript %}
  // Custom element for select button functionality
  class ElementSelect extends HTMLElement {
    constructor() {
      super();
      this.select = null;
      this.buttonText = null;
      this.button = null;
    }

    connectedCallback() {
      // Wait for DOM to be ready
      this.select = this.querySelector('select');
      this.button = this.querySelector('.element-button');
      
      if (this.select && this.button) {
        this.init();
      }
    }

    init() {
      // Find the button text element within the element.button
      this.buttonText = this.button.querySelector('.element-button__text') || this.button.querySelector('[data-button-text]');
      
      // Set initial text
      this.updateButtonText();
      
      // Add click handler to button to trigger select
      this.button.addEventListener('click', (e) => this.handleButtonClick(e));
      
      // Add keyboard handler for the select element
      this.select.addEventListener('keydown', (e) => this.handleKeydown(e));
      
      // Update on change
      this.select.addEventListener('change', () => this.updateButtonText());
      
      // Update on focus for better UX
      this.select.addEventListener('focus', () => this.handleFocus());
      this.select.addEventListener('blur', () => this.handleBlur());
    }

    handleButtonClick(e) {
      // Prevent default button behavior
      e.preventDefault();
      e.stopPropagation();
      
      // Trigger the native select
      this.select.focus();
      this.select.click();
    }

    handleKeydown(e) {
      // Handle keyboard navigation for the select
      switch (e.key) {
        case 'Enter':
        case ' ':
          e.preventDefault();
          this.select.click();
          break;
        case 'ArrowDown':
        case 'ArrowUp':
          // Let the native select handle arrow key navigation
          break;
        default:
          // Let other keys pass through
          break;
      }
    }

    updateButtonText() {
      if (!this.buttonText) return;
      
      const selectedOption = this.select.options[this.select.selectedIndex];
      if (selectedOption && selectedOption.value !== '') {
        this.buttonText.textContent = selectedOption.textContent;
      } else {
        this.buttonText.textContent = this.select.getAttribute('aria-label') || 'Select an option';
      }
    }

    handleFocus() {
      this.classList.add('element-select__wrapper--focused');
    }

    handleBlur() {
      this.classList.remove('element-select__wrapper--focused');
    }

    disconnectedCallback() {
      // Clean up event listeners
      if (this.button) {
        this.button.removeEventListener('click', (e) => this.handleButtonClick(e));
      }
      if (this.select) {
        this.select.removeEventListener('keydown', (e) => this.handleKeydown(e));
        this.select.removeEventListener('change', () => this.updateButtonText());
        this.select.removeEventListener('focus', () => this.handleFocus());
        this.select.removeEventListener('blur', () => this.handleBlur());
      }
    }
  }

  // Register the custom element
  customElements.define('element-select', ElementSelect);
{% endjavascript %}