Source: ds-option.js

/**
 * @file ds-option.js
 * @summary A custom Web Component that wraps a native `<option>` element.
 * @description
 * The `ds-option` component provides a styled and functional option element
 * for use within `<ds-select>` components. It maintains proper option behavior
 * and can be used as an alternative to native `<option>` elements.
 *
 * @element ds-option
 * @extends HTMLElement
 *
 * @attr {string} value - The value of the option when selected.
 * @attr {boolean} disabled - If present, the option cannot be selected.
 * @attr {boolean} selected - If present, the option is pre-selected.
 *
 * @property {string} value - Gets or sets the value of the option.
 * @property {boolean} selected - Gets or sets the selected state of the option.
 * @property {boolean} disabled - Gets or sets the disabled state of the option.
 *
 * @fires change - Fired when the option selection changes.
 *
 * @slot - Renders the option text content.
 *
 * @example
 * <!-- Basic option -->
 * <ds-option value="option1">Option 1</ds-option>
 *
 * @example
 * <!-- Pre-selected option -->
 * <ds-option value="default" selected>Default Option</ds-option>
 *
 * @example
 * <!-- Disabled option -->
 * <ds-option value="disabled" disabled>Disabled Option</ds-option>
 *
 * @example
 * <!-- Usage within ds-select -->
 * <ds-select name="category">
 *   <ds-option value="electronics">Electronics</ds-option>
 *   <ds-option value="clothing">Clothing</ds-option>
 *   <ds-option value="books">Books</ds-option>
 * </ds-select>
 */
class DsOption extends HTMLElement {
    constructor() {
        super();
        
        // Attach shadow root with open mode for experimentation
        const shadowRoot = this.attachShadow({ mode: 'open' });
        
        // Define the template with internal markup and styles
        const template = document.createElement('template');
        template.innerHTML = `
            <style>
                @import url('/src/design_system/styles.css');
                
                :host {
                    display: none; /* Hidden by default, shown when slotted into select */
                }
            </style>
            <div>
                <option part="option">
                    <slot></slot>
                </option>
            </div>
        `;
        
        // Append the template's content to the shadow root
        shadowRoot.appendChild(template.content.cloneNode(true));
        
        // Store reference to the internal option for attribute changes
        this.option = shadowRoot.querySelector('option');
        
        // Set up event listeners
        this.setupEventListeners();
    }
    
    /**
     * Defines which attributes the component observes for changes.
     * @returns {Array<string>} An array of attribute names to observe.
     */
    static get observedAttributes() {
        return ['value', 'disabled', 'selected'];
    }
    
    /**
     * Called when one of the component's observed attributes is added, removed, or changed.
     * @param {string} name - The name of the attribute that changed.
     * @param {string|null} oldValue - The attribute's old value.
     * @param {string|null} newValue - The attribute's new value.
     */
    attributeChangedCallback(name, oldValue, newValue) {
        if (oldValue === newValue) return; // No change
        
        switch (name) {
            case 'value':
                this.option.value = newValue || '';
                break;
                
            case 'disabled':
                if (this.hasAttribute('disabled')) {
                    this.option.disabled = true;
                } else {
                    this.option.disabled = false;
                }
                break;
                
            case 'selected':
                if (this.hasAttribute('selected')) {
                    this.option.selected = true;
                } else {
                    this.option.selected = false;
                }
                break;
        }
    }
    
    /**
     * Sets up event listeners for the option.
     */
    setupEventListeners() {
        // Options don't typically have interactive events, but we can listen for changes
        this.option.addEventListener('change', (event) => {
            const newEvent = new Event('change', {
                bubbles: true,
                composed: true,
                cancelable: true
            });
            this.dispatchEvent(newEvent);
        });
    }
    
    /**
     * Gets the value of the option.
     * @returns {string} The option's value.
     */
    get value() {
        return this.option.value;
    }
    
    /**
     * Sets the value of the option.
     * @param {string} val - The new value to set.
     */
    set value(val) {
        this.option.value = val;
    }
    
    /**
     * Gets the selected state of the option.
     * @returns {boolean} Whether the option is selected.
     */
    get selected() {
        return this.option.selected;
    }
    
    /**
     * Sets the selected state of the option.
     * @param {boolean} val - Whether to select the option.
     */
    set selected(val) {
        this.option.selected = val;
    }
    
    /**
     * Gets the disabled state of the option.
     * @returns {boolean} Whether the option is disabled.
     */
    get disabled() {
        return this.option.disabled;
    }
    
    /**
     * Sets the disabled state of the option.
     * @param {boolean} val - Whether to disable the option.
     */
    set disabled(val) {
        this.option.disabled = val;
    }
    
    /**
     * Called when the element is connected to the DOM.
     * Applies initial attributes.
     */
    connectedCallback() {
        // Apply initial attributes
        this.attributeChangedCallback('value', null, this.getAttribute('value'));
        this.attributeChangedCallback('disabled', null, this.getAttribute('disabled'));
        this.attributeChangedCallback('selected', null, this.getAttribute('selected'));
    }
}

// Register the custom element
customElements.define('ds-option', DsOption);