<div class="atm-form-select ">
    <select class="form-select" id="select" name="select">
        <option value="1" selected>Option One</option>
        <option value="2">Option Two</option>
        <option value="3">Option Three</option>
    </select>
</div>
<div class="atm-form-select {{modifier}}">
    <select 
        class="form-select"
        {{#if id}}id="{{id}}"{{/if}}
        {{#if name}}name="{{name}}"{{/if}}
        {{#if disabled}}disabled{{/if}}>
        {{#each options}} 
            <option value="{{value}}" {{#if selected}}selected{{/if}}>{{text}}</option>
        {{/each}}
    </select>
</div>
{
  "modifier": "",
  "id": "select",
  "name": "select",
  "disabled": false,
  "options": [
    {
      "value": "1",
      "text": "Option One",
      "selected": true
    },
    {
      "value": "2",
      "text": "Option Two",
      "selected": false
    },
    {
      "value": "3",
      "text": "Option Three",
      "selected": false
    }
  ]
}
  • Content:
    (function () {
        const selects = document.querySelectorAll('[data-custom-select]');
    
        const closeAll = () =>
            selects.forEach((c) => {
                const trigger = c.querySelector('.custom-select-trigger');
                const dropdown = c.querySelector('.custom-select-dropdown');
                if (trigger) trigger.setAttribute('aria-expanded', 'false');
                if (dropdown) dropdown.classList.remove('active');
            });
    
        selects.forEach((container) => {
            const native = container.querySelector('.form-select-native');
            const trigger = container.querySelector('.custom-select-trigger');
            const dropdown = container.querySelector('.custom-select-dropdown');
            const valueDisplay = container.querySelector('.custom-select-value');
    
            if (!trigger || !dropdown || !native) return;
    
            let isOpen = false;
    
            const open = () => {
                closeAll();
                isOpen = true;
                trigger.setAttribute('aria-expanded', 'true');
                dropdown.classList.add('active');
    
                // geef elke optie focus-mogelijkheid
                dropdown
                    .querySelectorAll('.custom-select-option')
                    .forEach((opt) => {
                        opt.tabIndex = 0;
                    });
    
                // focus op geselecteerde of eerste optie
                const toFocus =
                    dropdown.querySelector('.selected') ||
                    dropdown.firstElementChild;
                if (toFocus) {
                    requestAnimationFrame(() => toFocus.focus());
                }
            };
    
            const close = () => {
                if (!isOpen) return;
                isOpen = false;
                trigger.setAttribute('aria-expanded', 'false');
                dropdown.classList.remove('active');
            };
    
            const toggle = () => (isOpen ? close() : open());
    
            const select = (opt) => {
                if (!opt) return;
                const value = opt.dataset.value;
                const text = opt.textContent.trim();
    
                dropdown.querySelectorAll('.custom-select-option').forEach((o) => {
                    o.classList.remove('selected');
                    o.removeAttribute('aria-selected');
                });
                opt.classList.add('selected');
                opt.setAttribute('aria-selected', 'true');
                valueDisplay.textContent = text;
                native.value = value;
                native.dispatchEvent(new Event('change', { bubbles: true }));
                close();
                trigger.focus();
            };
    
            // klik op trigger
            trigger.addEventListener('click', (e) => {
                e.preventDefault();
                e.stopPropagation();
                toggle();
            });
    
            // toetsen op trigger
            trigger.addEventListener('keydown', (e) => {
                if (['Enter', ' ', 'ArrowDown'].includes(e.key)) {
                    e.preventDefault();
                    if (!isOpen) open();
                } else if (e.key === 'Escape') {
                    e.preventDefault();
                    close();
                }
            });
    
            // klik op optie via delegation
            dropdown.addEventListener('click', (e) => {
                const opt = e.target.closest('.custom-select-option');
                if (opt) {
                    e.stopPropagation();
                    select(opt);
                }
            });
    
            // toetsen op optie via delegation
            dropdown.addEventListener('keydown', (e) => {
                const opt = e.target.closest('.custom-select-option');
                if (!opt) return;
    
                switch (e.key) {
                    case 'Enter':
                    case ' ':
                        e.preventDefault();
                        select(opt);
                        break;
                    case 'Escape':
                        e.preventDefault();
                        close();
                        trigger.focus();
                        break;
                    case 'ArrowDown':
                        e.preventDefault();
                        opt.nextElementSibling?.focus();
                        break;
                    case 'ArrowUp':
                        e.preventDefault();
                        opt.previousElementSibling?.focus();
                        break;
                }
            });
    
            // klik buiten of scroll sluit dropdown
            document.addEventListener('click', (e) => {
                if (!container.contains(e.target)) close();
            });
    
            window.addEventListener(
                'scroll',
                () => {
                    if (isOpen) close();
                },
                { passive: true }
            );
        });
    })();
    
  • URL: /components/raw/select/select--choices.js
  • Filesystem Path: src\components\02-atoms\forms\select\select--choices.js
  • Size: 4.7 KB
  • Content:
    .atm-form-select {
        .form-select {
            @apply h-12;
            @apply w-full;
            @apply border-shade;
    
            &:focus {
                @apply border-cta;
            }
    
            &[disabled] {
                @apply bg-shade;
            }
        }
    }
    
    .atm-form-select--choices {
        @apply relative;
    
        /* Legacy select styling (fallback) */
        .form-select {
            @apply text-h1 font-display text-secondary-col-1;
            @apply border-none outline-none;
    
            &:focus {
                @apply border-none outline-none;
            }
    
            &[disabled] {
                @apply bg-shade;
            }
        }
    
        /* Custom select styling */
        .custom-select-wrapper {
            @apply relative w-max;
        }
    
        .custom-select-trigger {
            @apply text-left;
            @apply text-h5 md:text-h1 font-display text-secondary-col-1;
            @apply border-none;
            @apply cursor-pointer flex items-center;
            @apply transition-opacity duration-200;
    
            &:hover {
                @apply opacity-80;
            }
    
            &[aria-expanded='true'] {
                .custom-select-arrow i {
                    @apply rotate-180;
                }
            }
        }
    
        .custom-select-arrow {
            @apply ml-4 flex items-center;
    
            i {
                @apply transition-transform duration-200 text-xs md:text-h4;
            }
        }
    
        .custom-select-dropdown {
            @apply absolute top-full right-0 md:left-0 w-full min-w-[50vw] md:min-w-[20rem] text-left mt-3;
            @apply bg-white rounded-3xl shadow-[0_4px_6px_-2px_rgba(0,0,0,0.1)];
            @apply opacity-0 invisible pointer-events-none;
            @apply mt-3;
            @apply z-50 max-h-64 overflow-y-auto;
            transition: all 0.2s ease-in-out;
            list-style: none;
            padding: 0;
    
            &.active {
                @apply opacity-100 visible pointer-events-auto;
            }
        }
    
        .custom-select-option {
            @apply px-6 py-4 cursor-pointer;
            @apply text-lg font-display text-black;
            @apply transition-colors duration-150;
    
            &:hover {
                @apply bg-gray-100;
            }
    
            &.selected {
                @apply text-secondary-col-1;
    
                &:hover {
                    @apply text-secondary-col-1;
                }
            }
    
            &:first-child {
                @apply rounded-t-lg;
            }
    
            &:last-child {
                @apply rounded-b-lg;
            }
        }
    }
    
  • URL: /components/raw/select/select.css
  • Filesystem Path: src\components\02-atoms\forms\select\select.css
  • Size: 2.5 KB

No notes defined.