<div class="m-multi-select js-m-multi-select">
    <div class="m-multi-select__search">
        <input type="text" class="search-input a-input js-m-multi-select__input" data-multi-select-suggestions="suggestions" aria-controls="multiSelectSuggestionsBox" aria-autocomplete="list" aria-haspopup="true" role="combobox" placeholder="Sök städer..." aria-label="Sök städer" aria-describedby="multiSelectDescription" />
        <div class="m-multi-select__suggestions-box js-m-multi-select-suggestions-box" id="multiSelectSuggestionsBox" role="listbox" aria-live="assertive" aria-atomic="true"></div>
    </div>
    <ul class="m-multi-select-selected-items js-m-multi-select-selected-items" aria-live="assertive" aria-atomic="true"></ul>
    <div class="u-visuallyhidden" id="multiSelectDescription">Använd upp och ner-pilarna för att lägga till sökträffar och tabb plus enter för att ta bort valda sökträffar</div>
</div>

<script id="suggestions" type="application/json">
    [{
            "name": "New York"
        },
        {
            "name": "Los Angeles"
        },
        {
            "name": "Chicago"
        },
        {
            "name": "Houston"
        },
        {
            "name": "Phoenix"
        },
        {
            "name": "Philadelphia"
        },
        {
            "name": "San Antonio"
        },
        {
            "name": "San Diego"
        },
        {
            "name": "Dallas"
        },
        {
            "name": "San Jose"
        },
        {
            "name": "Austin"
        },
        {
            "name": "Jacksonville"
        },
        {
            "name": "Fort Worth"
        },
        {
            "name": "Columbus"
        },
        {
            "name": "San Francisco"
        },
        {
            "name": "Tokyo"
        },
        {
            "name": "Delhi"
        },
        {
            "name": "Shanghai"
        },
        {
            "name": "Sao Paulo"
        },
        {
            "name": "Mumbai"
        },
        {
            "name": "Beijing"
        },
        {
            "name": "Cairo"
        },
        {
            "name": "Dhaka"
        },
        {
            "name": "Mexico City"
        },
        {
            "name": "Osaka"
        },
        {
            "name": "Karachi"
        },
        {
            "name": "Chongqing"
        },
        {
            "name": "Istanbul"
        },
        {
            "name": "Buenos Aires"
        },
        {
            "name": "Kolkata"
        },
        {
            "name": "Kinshasa"
        },
        {
            "name": "Lagos"
        },
        {
            "name": "Manila"
        },
        {
            "name": "Tianjin"
        },
        {
            "name": "Rio de Janeiro"
        },
        {
            "name": "Guangzhou"
        },
        {
            "name": "Lahore"
        },
        {
            "name": "Moscow"
        },
        {
            "name": "Shenzhen"
        },
        {
            "name": "Bangalore"
        },
        {
            "name": "Paris"
        },
        {
            "name": "Bogota"
        },
        {
            "name": "Jakarta"
        },
        {
            "name": "Chennai"
        },
        {
            "name": "Lima"
        },
        {
            "name": "Bangkok"
        },
        {
            "name": "Seoul"
        },
        {
            "name": "Nagoya"
        },
        {
            "name": "Hyderabad"
        },
        {
            "name": "London"
        },
        {
            "name": "Tehran"
        },
        {
            "name": "Chengdu"
        },
        {
            "name": "Nanjing"
        },
        {
            "name": "Wuhan"
        },
        {
            "name": "Ho Chi Minh City"
        },
        {
            "name": "Luanda"
        },
        {
            "name": "Ahmedabad"
        },
        {
            "name": "Kuala Lumpur"
        },
        {
            "name": "Xi’an"
        },
        {
            "name": "Hong Kong"
        },
        {
            "name": "Dongguan"
        },
        {
            "name": "Hangzhou"
        },
        {
            "name": "Foshan"
        },
        {
            "name": "Shenyang"
        },
        {
            "name": "Riyadh"
        },
        {
            "name": "Baghdad"
        },
        {
            "name": "Santiago"
        },
        {
            "name": "Surat"
        },
        {
            "name": "Madrid"
        },
        {
            "name": "Suzhou"
        },
        {
            "name": "Pune"
        },
        {
            "name": "Harbin"
        },
        {
            "name": "Houston"
        },
        {
            "name": "Toronto"
        },
        {
            "name": "Dar es Salaam"
        },
        {
            "name": "Miami"
        },
        {
            "name": "Belo Horizonte"
        },
        {
            "name": "Singapore"
        },
        {
            "name": "Atlanta"
        },
        {
            "name": "Fukuoka"
        },
        {
            "name": "Khartoum"
        },
        {
            "name": "Barcelona"
        },
        {
            "name": "Johannesburg"
        },
        {
            "name": "Saint Petersburg"
        },
        {
            "name": "Qingdao"
        },
        {
            "name": "Dalian"
        },
        {
            "name": "Washington, D.C."
        },
        {
            "name": "Yangon"
        },
        {
            "name": "Alexandria"
        },
        {
            "name": "Jinan"
        },
        {
            "name": "Guadalajara"
        },
        {
            "name": "Sydney"
        },
        {
            "name": "Melbourne"
        },
        {
            "name": "Montreal"
        },
        {
            "name": "Ankara"
        },
        {
            "name": "Recife"
        },
        {
            "name": "Durban"
        },
        {
            "name": "Porto Alegre"
        },
        {
            "name": "Dusseldorf"
        },
        {
            "name": "Hamburg"
        },
        {
            "name": "Cape Town"
        },
        {
            "name": "Stockholm"
        }
    ]
</script>
<div class="m-multi-select js-m-multi-select">
	<div class="m-multi-select__search">
		<input type="text" class="search-input a-input js-m-multi-select__input" data-multi-select-suggestions="suggestions" aria-controls="multiSelectSuggestionsBox" aria-autocomplete="list" aria-haspopup="true" role="combobox" placeholder="Sök städer..." aria-label="Sök städer" aria-describedby="multiSelectDescription"/>
		<div class="m-multi-select__suggestions-box js-m-multi-select-suggestions-box" id="multiSelectSuggestionsBox" role="listbox" aria-live="assertive" aria-atomic="true"></div>
	</div>
	<ul class="m-multi-select-selected-items js-m-multi-select-selected-items" aria-live="assertive" aria-atomic="true"></ul>
	<div class="u-visuallyhidden" id="multiSelectDescription">Använd upp och ner-pilarna för att lägga till sökträffar och tabb plus enter för att ta bort valda sökträffar</div>
</div>

<script id="suggestions" type="application/json">
	[
		{
		"name":"New York"
		},
		{
		"name":"Los Angeles"
		},
		{
		"name":"Chicago"
		},
		{
		"name":"Houston"
		},
		{
		"name":"Phoenix"
		},
		{
		"name":"Philadelphia"
		},
		{
		"name":"San Antonio"
		},
		{
		"name":"San Diego"
		},
		{
		"name":"Dallas"
		},
		{
		"name":"San Jose"
		},
		{
		"name":"Austin"
		},
		{
		"name":"Jacksonville"
		},
		{
		"name":"Fort Worth"
		},
		{
		"name":"Columbus"
		},
		{
		"name":"San Francisco"
		},
		{
		"name":"Tokyo"
		},
		{
		"name":"Delhi"
		},
		{
		"name":"Shanghai"
		},
		{
		"name":"Sao Paulo"
		},
		{
		"name":"Mumbai"
		},
		{
		"name":"Beijing"
		},
		{
		"name":"Cairo"
		},
		{
		"name":"Dhaka"
		},
		{
		"name":"Mexico City"
		},
		{
		"name":"Osaka"
		},
		{
		"name":"Karachi"
		},
		{
		"name":"Chongqing"
		},
		{
		"name":"Istanbul"
		},
		{
		"name":"Buenos Aires"
		},
		{
		"name":"Kolkata"
		},
		{
		"name":"Kinshasa"
		},
		{
		"name":"Lagos"
		},
		{
		"name":"Manila"
		},
		{
		"name":"Tianjin"
		},
		{
		"name":"Rio de Janeiro"
		},
		{
		"name":"Guangzhou"
		},
		{
		"name":"Lahore"
		},
		{
		"name":"Moscow"
		},
		{
		"name":"Shenzhen"
		},
		{
		"name":"Bangalore"
		},
		{
		"name":"Paris"
		},
		{
		"name":"Bogota"
		},
		{
		"name":"Jakarta"
		},
		{
		"name":"Chennai"
		},
		{
		"name":"Lima"
		},
		{
		"name":"Bangkok"
		},
		{
		"name":"Seoul"
		},
		{
		"name":"Nagoya"
		},
		{
		"name":"Hyderabad"
		},
		{
		"name":"London"
		},
		{
		"name":"Tehran"
		},
		{
		"name":"Chengdu"
		},
		{
		"name":"Nanjing"
		},
		{
		"name":"Wuhan"
		},
		{
		"name":"Ho Chi Minh City"
		},
		{
		"name":"Luanda"
		},
		{
		"name":"Ahmedabad"
		},
		{
		"name":"Kuala Lumpur"
		},
		{
		"name":"Xi’an"
		},
		{
		"name":"Hong Kong"
		},
		{
		"name":"Dongguan"
		},
		{
		"name":"Hangzhou"
		},
		{
		"name":"Foshan"
		},
		{
		"name":"Shenyang"
		},
		{
		"name":"Riyadh"
		},
		{
		"name":"Baghdad"
		},
		{
		"name":"Santiago"
		},
		{
		"name":"Surat"
		},
		{
		"name":"Madrid"
		},
		{
		"name":"Suzhou"
		},
		{
		"name":"Pune"
		},
		{
		"name":"Harbin"
		},
		{
		"name":"Houston"
		},
		{
		"name":"Toronto"
		},
		{
		"name":"Dar es Salaam"
		},
		{
		"name":"Miami"
		},
		{
		"name":"Belo Horizonte"
		},
		{
		"name":"Singapore"
		},
		{
		"name":"Atlanta"
		},
		{
		"name":"Fukuoka"
		},
		{
		"name":"Khartoum"
		},
		{
		"name":"Barcelona"
		},
		{
		"name":"Johannesburg"
		},
		{
		"name":"Saint Petersburg"
		},
		{
		"name":"Qingdao"
		},
		{
		"name":"Dalian"
		},
		{
		"name":"Washington, D.C."
		},
		{
		"name":"Yangon"
		},
		{
		"name":"Alexandria"
		},
		{
		"name":"Jinan"
		},
		{
		"name":"Guadalajara"
		},
		{
		"name":"Sydney"
		},
		{
		"name":"Melbourne"
		},
		{
		"name":"Montreal"
		},
		{
		"name":"Ankara"
		},
		{
		"name":"Recife"
		},
		{
		"name":"Durban"
		},
		{
		"name":"Porto Alegre"
		},
		{
		"name":"Dusseldorf"
		},
		{
		"name":"Hamburg"
		},
		{
		"name":"Cape Town"
		},
		{
		"name":"Stockholm"
		}
	]
</script>
/* No context defined. */
  • Content:
    @charset "UTF-8";
    
    @include molecule(multi-select) {
    	position: relative;
    
    	&::before {
    		@extend %u-visuallyhidden;
    
    		content: $namespace;
    	}
    
    	@include e(search) {
    		position: relative;
    	}
    
    	@include e(suggestions-box) {
    		position: absolute;
    		border-top: none;
    		z-index: z_index(foreground);
    		top: 100%;
    		left: 0;
    		right: 0;
    		background-color: $color-snow;
    		overflow-y: auto;
    		max-height: rhythm(15);
    		border-bottom-left-radius: $border-radius;
    		border-bottom-right-radius: $border-radius;
    
    		@extend %box-shadow;
    	}
    
    	@include e(suggestion-btn) {
    		padding: rhythm(1);
    		cursor: pointer;
    		background-color: $color-snow;
    		border: none;
    		border-bottom: 1px solid $color-concrete;
    		width: 100%;
    		text-align: left;
    
    		&:hover,
    		&.autocomplete-active {
    			background-color: $color-ocean-dark;
    			color: $color-snow;
    		}
    	}
    
    	@include e(tag){
    		margin-bottom: rhythm(1);
    		background-color: $color-ash;
    		text-transform: none;
    		font-size: $size-medium;
    
    		&:hover,
    		&:focus {
    			background-color: $color-ash;
    			color: $color-cyberspace;
    		}
    	}
    }
    
    /* Selected items container */
    @include molecule(multi-select-selected-items) {
    	margin-top: -#{rhythm(1)};
    	padding: rhythm(2) rhythm(1) 0 rhythm(1);
    	border: 1px solid #d4d4d4;
    	background-color: $color-concrete;
    	border-bottom-left-radius: $border-radius;
    	border-bottom-right-radius: $border-radius;
    
    	&:empty {
    		display: none;
    	}
    
    	@include e(remove-btn) {
    		margin-left: 5px;
    		border: none;
    		background-color: #d80000;
    		color: white;
    		border-radius: 50%;
    		cursor: pointer;
    		display: flex;
    		align-items: center;
    		justify-content: center;
    		line-height: 1;
    		width: $icon-size-small;
    		height: $icon-size-small;
    		position: relative;
    		transform: translateX(rhythm(0.5));
    
    		&::after {
    			content: '';
    			display: block;
    			width: 100%;
    			height: 100%;
    			position: absolute;
    			top: 0;
    			right: 0;
    			bottom: 0;
    			left: 0;
    			background-image: url(data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4KPCEtLSBHZW5lcmF0b3I6IEFkb2JlIElsbHVzdHJhdG9yIDI4LjIuMCwgU1ZHIEV4cG9ydCBQbHVnLUluIC4gU1ZHIFZlcnNpb246IDYuMDAgQnVpbGQgMCkgIC0tPgo8c3ZnIHZlcnNpb249IjEuMSIgaWQ9IkxheWVyXzEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIHg9IjBweCIgeT0iMHB4IgoJIHZpZXdCb3g9IjAgMCAxNiAxNiIgc3R5bGU9ImVuYWJsZS1iYWNrZ3JvdW5kOm5ldyAwIDAgMTYgMTY7IiB4bWw6c3BhY2U9InByZXNlcnZlIj4KPHN0eWxlIHR5cGU9InRleHQvY3NzIj4KCS5zdDB7ZmlsbC1ydWxlOmV2ZW5vZGQ7Y2xpcC1ydWxlOmV2ZW5vZGQ7ZmlsbDojRkZGRkZGO30KPC9zdHlsZT4KPHBvbHlnb24gY2xhc3M9InN0MCIgcG9pbnRzPSI4LDEwIDIsMTYgMCwxNCA2LDggMCwyIDIsMCA4LDYgMTQsMCAxNiwyIDEwLDggMTYsMTQgMTQsMTYgIi8+Cjwvc3ZnPgo=);
    			background-repeat: no-repeat;
    			background-position: center center;
    			background-size: calc($icon-size-small / 2) calc($icon-size-small / 2);
    		}
    
    		&:hover,
    		&:focus {
    			background-color: $color-cyberspace;
    		}
    	}
    }
    
  • URL: /components/raw/multi-select/_multi-select.scss
  • Filesystem Path: src/molecules/multi-select/_multi-select.scss
  • Size: 2.9 KB
  • Content:
    import className from '../../assets/js/className';
    
    class MultiSelect {
    	constructor(el) {
    		this.element = el;
    		this.baseClassName = 'm-multi-select';
    		this.currentFocus = -1;
    		this.input = this.element.querySelector(`.js-${this.baseClassName}__input`);
    		this.suggestionsBox = this.element.querySelector(`.js-${this.baseClassName}-suggestions-box`);
    		this.selectedItemsList = this.element.querySelector('.js-m-multi-select-selected-items');
    		this.selectedItems = [];
    		this.data = [];
    
    		this.getData();
    		this.attach();
    	}
    
    	getData() {
    		const id = this.input.getAttribute('data-multi-select-suggestions');
    		const el = document.getElementById(id);
    
    		if (!el) {
    			this.data = [];
    			return;
    		}
    
    		this.data = JSON.parse(el.textContent);
    	}
    
    	attach() {
    		this.input.addEventListener('input', this.onInput);
    		this.input.addEventListener('keydown', this.onKeyDown);
    		this.suggestionsBox.addEventListener('click', this.onClick);
    	}
    
    	setFocus(index) {
    		this.currentFocus = index;
    	}
    
    	resetFocus() {
    		this.setFocus(-1);
    	}
    
    	clearSuggestions() {
    		this.suggestionsBox.innerHTML = '';
    	}
    
    	filterData(query) {
    		return this.data
    			.filter((item) => item.name.toLowerCase().startsWith(query.toLowerCase()))
    			.filter((item) => !this.selectedItems.includes(item.name));
    	}
    
    	populateSuggestions(suggestions) {
    		const cls = className(`${this.baseClassName}__suggestion-btn`);
    		this.suggestionsBox.innerHTML = suggestions.map((item) => `<button class='${cls}' tabindex='0'>${item.name}</button>`).join('');
    	}
    
    	onInput = () => {
    		const { value } = this.input;
    
    		// Clear suggestions if less than 2 characters are typed
    		if (value.length < 2) {
    			this.clearSuggestions();
    
    			return;
    		}
    
    		const suggestions = this.filterData(value);
    
    		this.populateSuggestions(suggestions);
    		this.resetFocus();
    	}
    
    	removeItem(item, index) {
    		const selectedItemsList = this.element.querySelector('.js-m-multi-select-selected-items');
    		selectedItemsList.removeChild(item);
    
    		const remainingItems = selectedItemsList.getElementsByTagName('div');
    
    		// Focus management: set focus to the next item, or the search input if no items left
    		if (remainingItems.length > 0) {
    			if (index < remainingItems.length) {
    				remainingItems[index].getElementsByTagName('button')[0].focus();
    			} else {
    				remainingItems[remainingItems.length - 1].getElementsByTagName('button')[0].focus();
    			}
    		} else {
    			this.input.focus();
    		}
    
    		this.selectedItems = this.selectedItems
    			.filter((name) => name !== item.firstChild.textContent.trim());
    	}
    
    	addItem(item) {
    		const newItem = document.createElement('li');
    		newItem.textContent = `${item} `;
    		newItem.classList.add(className('a-tag'));
    		newItem.classList.add(className(`${this.baseClassName}__tag`));
    
    		const removeBtn = document.createElement('button');
    		removeBtn.classList.add(className(`${this.baseClassName}-selected-items__remove-btn`));
    
    		const buttonTextContainer = document.createElement('span');
    		buttonTextContainer.classList.add('u-visuallyhidden');
    		removeBtn.appendChild(buttonTextContainer);
    		buttonTextContainer.textContent = `Ta bort ${item}`; // Accessibility label for screen readers
    
    		// Event listener for removing the selected item
    		removeBtn.addEventListener('click', () => {
    			this.removeItem(newItem, Array.from(this.selectedItemsList.children).indexOf(newItem));
    		});
    
    		newItem.appendChild(removeBtn);
    
    		this.selectedItemsList.appendChild(newItem);
    		this.selectedItems.push(item);
    	}
    
    	removeHighlight() {
    		const items = this.suggestionsBox.getElementsByClassName(`${this.baseClassName}__suggestion-btn`);
    
    		[].forEach.call(items, (item) => {
    			item.classList.remove('autocomplete-active');
    		});
    	}
    
    	highlight(direction) {
    		const items = this.suggestionsBox.getElementsByClassName(`${this.baseClassName}__suggestion-btn`);
    		let focus = this.currentFocus;
    
    		if (direction === 'down') {
    			focus = (focus >= items.length - 1) ? 0 : focus + 1;
    		} else {
    			focus = (focus <= 0) ? items.length - 1 : focus - 1;
    		}
    
    		this.setFocus(focus);
    		this.removeHighlight();
    
    		items[this.currentFocus].classList.add('autocomplete-active');
    		items[this.currentFocus].scrollIntoView({ block: 'nearest' });
    	}
    
    	selectHighlighted() {
    		const items = this.suggestionsBox.getElementsByClassName(`${this.baseClassName}__suggestion-btn`);
    
    		if (this.currentFocus > -1 && items[this.currentFocus]) {
    			this.addItem(items[this.currentFocus].textContent);
    			this.clearSuggestions();
    			this.input.value = '';
    			this.resetFocus();
    		}
    	}
    
    	onKeyDown = (e) => {
    		if (e.keyCode === 40) {
    			this.highlight('down');
    		} else if (e.keyCode === 38) {
    			this.highlight('up');
    		} else if (e.keyCode === 13) {
    			e.preventDefault();
    
    			this.selectHighlighted();
    		}
    	}
    
    	onClick = (e) => {
    		if (e.target.classList.contains(className(`${this.baseClassName}__suggestion-btn`))) {
    			this.addItem(e.target.textContent);
    			this.clearSuggestions();
    			this.input.value = '';
    		}
    	}
    }
    
    const multiSelectElements = document.querySelectorAll('.js-m-multi-select');
    
    if (multiSelectElements) {
    	[].forEach.call(multiSelectElements, (el) => new MultiSelect(el));
    }
    
  • URL: /components/raw/multi-select/multi-select.js
  • Filesystem Path: src/molecules/multi-select/multi-select.js
  • Size: 5.1 KB

Accessible Multi Select with Autocomplete

Use your mouse or keyboard to select and remove items. Compatible with screen readers.