mirror of
https://github.com/angular/angular
synced 2026-05-24 09:28:37 +00:00
docs: aria autocomplete examples
This commit is contained in:
parent
d4595791d6
commit
b2080c8fb0
28 changed files with 3565 additions and 508 deletions
|
|
@ -1,91 +1,101 @@
|
|||
@import url('https://fonts.googleapis.com/icon?family=Material+Symbols+Outlined');
|
||||
|
||||
:host {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
font-family: var(--inter-font);
|
||||
}
|
||||
|
||||
.combobox-container {
|
||||
max-width: 400px;
|
||||
margin: 20px;
|
||||
}
|
||||
|
||||
label {
|
||||
display: block;
|
||||
margin-bottom: 8px;
|
||||
font-weight: 500;
|
||||
color: #e0e0e0;
|
||||
}
|
||||
|
||||
.input-container {
|
||||
.autocomplete {
|
||||
display: flex;
|
||||
position: relative;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.combobox-input {
|
||||
.material-symbols-outlined {
|
||||
font-size: 1.25rem;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.search-icon {
|
||||
left: 0.75rem;
|
||||
position: absolute;
|
||||
color: var(--quaternary-contrast);
|
||||
}
|
||||
|
||||
[ngComboboxInput] {
|
||||
width: 13rem;
|
||||
font-size: 1rem;
|
||||
border-radius: 0.25rem;
|
||||
padding: 0.75rem 0.5rem 0.75rem 2.5rem;
|
||||
color: var(--primary-contrast);
|
||||
outline-color: var(--hot-pink);
|
||||
border: 1px solid var(--quinary-contrast);
|
||||
background-color: var(--page-background);
|
||||
}
|
||||
|
||||
[ngComboboxInput]::placeholder {
|
||||
color: var(--quaternary-contrast);
|
||||
}
|
||||
|
||||
[ngCombobox]:has([aria-expanded='false']) .popup {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.popup {
|
||||
width: 100%;
|
||||
padding: 10px 12px;
|
||||
border: 1px solid #404040;
|
||||
border-radius: 4px;
|
||||
font-size: 16px;
|
||||
box-sizing: border-box;
|
||||
background-color: #1a1a1a;
|
||||
color: #e0e0e0;
|
||||
margin-top: 8px;
|
||||
padding: 0.5rem;
|
||||
max-height: 11rem;
|
||||
border-radius: 0.5rem;
|
||||
background-color: var(--septenary-contrast);
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.combobox-input::placeholder {
|
||||
color: #888;
|
||||
.no-results {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.combobox-input:focus {
|
||||
outline: none;
|
||||
border-color: #4a9eff;
|
||||
background-color: #1f1f1f;
|
||||
[ngListbox] {
|
||||
gap: 2px;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
overflow: auto;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.popover {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
border: 1px solid #404040;
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.5);
|
||||
background: #1a1a1a;
|
||||
max-height: 300px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.listbox {
|
||||
padding: 4px 0;
|
||||
}
|
||||
|
||||
.option {
|
||||
padding: 10px 12px;
|
||||
[ngOption] {
|
||||
display: flex;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
color: #e0e0e0;
|
||||
align-items: center;
|
||||
margin: 1px;
|
||||
padding: 0 1rem;
|
||||
min-height: 2.25rem;
|
||||
border-radius: 0.5rem;
|
||||
}
|
||||
|
||||
.option:hover {
|
||||
background-color: #2a2a2a;
|
||||
[ngOption]:hover {
|
||||
background-color: color-mix(in srgb, var(--primary-contrast) 5%, transparent);
|
||||
}
|
||||
|
||||
.option[data-active] {
|
||||
background-color: #2d4a6e;
|
||||
color: #ffffff;
|
||||
[ngOption][data-active='true'] {
|
||||
outline-offset: -2px;
|
||||
outline: 2px solid var(--hot-pink);
|
||||
}
|
||||
|
||||
.option[aria-selected='true'] {
|
||||
background-color: #4a9eff;
|
||||
color: #000000;
|
||||
[ngOption][aria-selected='true'] {
|
||||
color: var(--hot-pink);
|
||||
background-color: color-mix(in srgb, var(--hot-pink) 5%, transparent);
|
||||
}
|
||||
|
||||
.info {
|
||||
margin: 20px;
|
||||
padding: 16px;
|
||||
background-color: #1f1f1f;
|
||||
border-radius: 4px;
|
||||
border-left: 4px solid #4a9eff;
|
||||
color: #e0e0e0;
|
||||
[ngOption]:not([aria-selected='true']) .check-icon {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.info p {
|
||||
margin: 8px 0;
|
||||
.option-label {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.check-icon {
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,36 +1,33 @@
|
|||
<div ngCombobox #combobox="ngCombobox" filterMode="auto-select" class="combobox-container">
|
||||
<label for="state-input">Select a state:</label>
|
||||
<div class="input-container">
|
||||
<div ngCombobox filterMode="auto-select">
|
||||
<div #origin class="autocomplete">
|
||||
<span class="search-icon material-symbols-outlined">search</span>
|
||||
<input
|
||||
id="state-input"
|
||||
aria-label="Label dropdown"
|
||||
placeholder="Select a country"
|
||||
[(ngModel)]="query"
|
||||
ngComboboxInput
|
||||
placeholder="Search states..."
|
||||
[(value)]="searchString"
|
||||
class="combobox-input"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div popover="manual" #popover class="popover">
|
||||
<ng-template ngComboboxPopupContainer>
|
||||
<div ngListbox class="listbox">
|
||||
@for (option of options(); track option) {
|
||||
<div class="option" ngOption [value]="option" [label]="option">
|
||||
<span>{{ option }}</span>
|
||||
</div>
|
||||
<ng-template ngComboboxPopupContainer>
|
||||
<ng-template
|
||||
[cdkConnectedOverlay]="{origin, usePopover: 'inline', matchWidth: true}"
|
||||
[cdkConnectedOverlayOpen]="true"
|
||||
>
|
||||
<div class="popup">
|
||||
@if (countries().length === 0) {
|
||||
<div class="no-results">No results found</div>
|
||||
}
|
||||
|
||||
<div ngListbox>
|
||||
@for (country of countries(); track country) {
|
||||
<div ngOption [value]="country" [label]="country">
|
||||
<span class="option-label">{{country}}</span>
|
||||
<span class="check-icon material-symbols-outlined">check</span>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</ng-template>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="info">
|
||||
<p>
|
||||
<strong>Auto-select mode:</strong> The input updates automatically as you type to match the
|
||||
first option.
|
||||
</p>
|
||||
@if (searchString()) {
|
||||
<p>
|
||||
Current value: <strong>{{ searchString() }}</strong>
|
||||
</p>
|
||||
}
|
||||
</ng-template>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,108 +1,261 @@
|
|||
import {Combobox, ComboboxInput, ComboboxPopupContainer} from '@angular/aria/combobox';
|
||||
import {Listbox, Option} from '@angular/aria/listbox';
|
||||
import {OverlayModule} from '@angular/cdk/overlay';
|
||||
import {
|
||||
afterRenderEffect,
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
computed,
|
||||
ElementRef,
|
||||
signal,
|
||||
viewChild,
|
||||
viewChildren,
|
||||
} from '@angular/core';
|
||||
import {FormsModule} from '@angular/forms';
|
||||
|
||||
@Component({
|
||||
selector: 'app-root',
|
||||
templateUrl: 'app.component.html',
|
||||
styleUrl: 'app.component.css',
|
||||
imports: [Combobox, ComboboxInput, ComboboxPopupContainer, Listbox, Option],
|
||||
imports: [
|
||||
Combobox,
|
||||
ComboboxInput,
|
||||
ComboboxPopupContainer,
|
||||
Listbox,
|
||||
Option,
|
||||
OverlayModule,
|
||||
FormsModule,
|
||||
],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class App {
|
||||
popover = viewChild<ElementRef>('popover');
|
||||
listbox = viewChild<Listbox<any>>(Listbox);
|
||||
combobox = viewChild<Combobox<any>>(Combobox);
|
||||
/** The combobox listbox popup. */
|
||||
listbox = viewChild<Listbox<string>>(Listbox);
|
||||
|
||||
searchString = signal('');
|
||||
/** The options available in the listbox. */
|
||||
options = viewChildren<Option<string>>(Option);
|
||||
|
||||
options = computed(() =>
|
||||
states.filter((state) => state.toLowerCase().startsWith(this.searchString().toLowerCase())),
|
||||
/** A reference to the ng aria combobox. */
|
||||
combobox = viewChild<Combobox<string>>(Combobox);
|
||||
|
||||
/** The query string used to filter the list of countries. */
|
||||
query = signal('');
|
||||
|
||||
/** The list of countries filtered by the query. */
|
||||
countries = computed(() =>
|
||||
ALL_COUNTRIES.filter((country) => country.toLowerCase().startsWith(this.query().toLowerCase())),
|
||||
);
|
||||
|
||||
constructor() {
|
||||
// Scrolls to the active item when the active option changes.
|
||||
// The slight delay here is to ensure animations are done before scrolling.
|
||||
afterRenderEffect(() => {
|
||||
const popover = this.popover()!;
|
||||
const combobox = this.combobox()!;
|
||||
combobox.expanded() ? this.showPopover() : popover.nativeElement.hidePopover();
|
||||
this.listbox()?.scrollActiveItemIntoView();
|
||||
const option = this.options().find((opt) => opt.active());
|
||||
setTimeout(() => option?.element.scrollIntoView({block: 'nearest'}), 50);
|
||||
});
|
||||
}
|
||||
|
||||
showPopover() {
|
||||
const popover = this.popover()!;
|
||||
const combobox = this.combobox()!;
|
||||
|
||||
const comboboxRect = combobox.inputElement()?.getBoundingClientRect();
|
||||
const popoverEl = popover.nativeElement;
|
||||
|
||||
if (comboboxRect) {
|
||||
popoverEl.style.width = `${comboboxRect.width}px`;
|
||||
popoverEl.style.top = `${comboboxRect.bottom + 4}px`;
|
||||
popoverEl.style.left = `${comboboxRect.left - 1}px`;
|
||||
}
|
||||
|
||||
popover.nativeElement.showPopover();
|
||||
// Resets the listbox scroll position when the combobox is closed.
|
||||
afterRenderEffect(() => {
|
||||
if (!this.combobox()?.expanded()) {
|
||||
setTimeout(() => this.listbox()?.element.scrollTo(0, 0), 150);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const states = [
|
||||
'Alabama',
|
||||
'Alaska',
|
||||
'Arizona',
|
||||
'Arkansas',
|
||||
'California',
|
||||
'Colorado',
|
||||
'Connecticut',
|
||||
'Delaware',
|
||||
'Florida',
|
||||
const ALL_COUNTRIES = [
|
||||
'Afghanistan',
|
||||
'Albania',
|
||||
'Algeria',
|
||||
'Andorra',
|
||||
'Angola',
|
||||
'Antigua and Barbuda',
|
||||
'Argentina',
|
||||
'Armenia',
|
||||
'Australia',
|
||||
'Austria',
|
||||
'Azerbaijan',
|
||||
'Bahamas',
|
||||
'Bahrain',
|
||||
'Bangladesh',
|
||||
'Barbados',
|
||||
'Belarus',
|
||||
'Belgium',
|
||||
'Belize',
|
||||
'Benin',
|
||||
'Bhutan',
|
||||
'Bolivia',
|
||||
'Bosnia and Herzegovina',
|
||||
'Botswana',
|
||||
'Brazil',
|
||||
'Brunei',
|
||||
'Bulgaria',
|
||||
'Burkina Faso',
|
||||
'Burundi',
|
||||
'Cabo Verde',
|
||||
'Cambodia',
|
||||
'Cameroon',
|
||||
'Canada',
|
||||
'Central African Republic',
|
||||
'Chad',
|
||||
'Chile',
|
||||
'China',
|
||||
'Colombia',
|
||||
'Comoros',
|
||||
'Congo (Congo-Brazzaville)',
|
||||
'Costa Rica',
|
||||
"Côte d'Ivoire",
|
||||
'Croatia',
|
||||
'Cuba',
|
||||
'Cyprus',
|
||||
'Czechia (Czech Republic)',
|
||||
'Democratic Republic of the Congo',
|
||||
'Denmark',
|
||||
'Djibouti',
|
||||
'Dominica',
|
||||
'Dominican Republic',
|
||||
'Ecuador',
|
||||
'Egypt',
|
||||
'El Salvador',
|
||||
'Equatorial Guinea',
|
||||
'Eritrea',
|
||||
'Estonia',
|
||||
'Eswatini (fmr. ""Swaziland"")',
|
||||
'Ethiopia',
|
||||
'Fiji',
|
||||
'Finland',
|
||||
'France',
|
||||
'Gabon',
|
||||
'Gambia',
|
||||
'Georgia',
|
||||
'Hawaii',
|
||||
'Idaho',
|
||||
'Illinois',
|
||||
'Indiana',
|
||||
'Iowa',
|
||||
'Kansas',
|
||||
'Kentucky',
|
||||
'Louisiana',
|
||||
'Maine',
|
||||
'Maryland',
|
||||
'Massachusetts',
|
||||
'Michigan',
|
||||
'Minnesota',
|
||||
'Mississippi',
|
||||
'Missouri',
|
||||
'Montana',
|
||||
'Nebraska',
|
||||
'Nevada',
|
||||
'New Hampshire',
|
||||
'New Jersey',
|
||||
'New Mexico',
|
||||
'New York',
|
||||
'North Carolina',
|
||||
'North Dakota',
|
||||
'Ohio',
|
||||
'Oklahoma',
|
||||
'Oregon',
|
||||
'Pennsylvania',
|
||||
'Rhode Island',
|
||||
'South Carolina',
|
||||
'South Dakota',
|
||||
'Tennessee',
|
||||
'Texas',
|
||||
'Utah',
|
||||
'Vermont',
|
||||
'Virginia',
|
||||
'Washington',
|
||||
'West Virginia',
|
||||
'Wisconsin',
|
||||
'Wyoming',
|
||||
'Germany',
|
||||
'Ghana',
|
||||
'Greece',
|
||||
'Grenada',
|
||||
'Guatemala',
|
||||
'Guinea',
|
||||
'Guinea-Bissau',
|
||||
'Guyana',
|
||||
'Haiti',
|
||||
'Holy See',
|
||||
'Honduras',
|
||||
'Hungary',
|
||||
'Iceland',
|
||||
'India',
|
||||
'Indonesia',
|
||||
'Iran',
|
||||
'Iraq',
|
||||
'Ireland',
|
||||
'Israel',
|
||||
'Italy',
|
||||
'Jamaica',
|
||||
'Japan',
|
||||
'Jordan',
|
||||
'Kazakhstan',
|
||||
'Kenya',
|
||||
'Kiribati',
|
||||
'Kuwait',
|
||||
'Kyrgyzstan',
|
||||
'Laos',
|
||||
'Latvia',
|
||||
'Lebanon',
|
||||
'Lesotho',
|
||||
'Liberia',
|
||||
'Libya',
|
||||
'Liechtenstein',
|
||||
'Lithuania',
|
||||
'Luxembourg',
|
||||
'Madagascar',
|
||||
'Malawi',
|
||||
'Malaysia',
|
||||
'Maldives',
|
||||
'Mali',
|
||||
'Malta',
|
||||
'Marshall Islands',
|
||||
'Mauritania',
|
||||
'Mauritius',
|
||||
'Mexico',
|
||||
'Micronesia',
|
||||
'Moldova',
|
||||
'Monaco',
|
||||
'Mongolia',
|
||||
'Montenegro',
|
||||
'Morocco',
|
||||
'Mozambique',
|
||||
'Myanmar (formerly Burma)',
|
||||
'Namibia',
|
||||
'Nauru',
|
||||
'Nepal',
|
||||
'Netherlands',
|
||||
'New Zealand',
|
||||
'Nicaragua',
|
||||
'Niger',
|
||||
'Nigeria',
|
||||
'North Korea',
|
||||
'North Macedonia',
|
||||
'Norway',
|
||||
'Oman',
|
||||
'Pakistan',
|
||||
'Palau',
|
||||
'Palestine State',
|
||||
'Panama',
|
||||
'Papua New Guinea',
|
||||
'Paraguay',
|
||||
'Peru',
|
||||
'Philippines',
|
||||
'Poland',
|
||||
'Portugal',
|
||||
'Qatar',
|
||||
'Romania',
|
||||
'Russia',
|
||||
'Rwanda',
|
||||
'Saint Kitts and Nevis',
|
||||
'Saint Lucia',
|
||||
'Saint Vincent and the Grenadines',
|
||||
'Samoa',
|
||||
'San Marino',
|
||||
'Sao Tome and Principe',
|
||||
'Saudi Arabia',
|
||||
'Senegal',
|
||||
'Serbia',
|
||||
'Seychelles',
|
||||
'Sierra Leone',
|
||||
'Singapore',
|
||||
'Slovakia',
|
||||
'Slovenia',
|
||||
'Solomon Islands',
|
||||
'Somalia',
|
||||
'South Africa',
|
||||
'South Korea',
|
||||
'South Sudan',
|
||||
'Spain',
|
||||
'Sri Lanka',
|
||||
'Sudan',
|
||||
'Suriname',
|
||||
'Sweden',
|
||||
'Switzerland',
|
||||
'Syria',
|
||||
'Tajikistan',
|
||||
'Tanzania',
|
||||
'Thailand',
|
||||
'Timor-Leste',
|
||||
'Togo',
|
||||
'Tonga',
|
||||
'Trinidad and Tobago',
|
||||
'Tunisia',
|
||||
'Turkey',
|
||||
'Turkmenistan',
|
||||
'Tuvalu',
|
||||
'Uganda',
|
||||
'Ukraine',
|
||||
'United Arab Emirates',
|
||||
'United Kingdom',
|
||||
'United States of America',
|
||||
'Uruguay',
|
||||
'Uzbekistan',
|
||||
'Vanuatu',
|
||||
'Venezuela',
|
||||
'Vietnam',
|
||||
'Yemen',
|
||||
'Zambia',
|
||||
'Zimbabwe',
|
||||
];
|
||||
|
|
|
|||
|
|
@ -0,0 +1,108 @@
|
|||
@import url('https://fonts.googleapis.com/icon?family=Material+Symbols+Outlined');
|
||||
|
||||
:host {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
font-family: var(--inter-font);
|
||||
--primary: var(--hot-pink);
|
||||
}
|
||||
|
||||
.material-autocomplete {
|
||||
display: flex;
|
||||
position: relative;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.material-symbols-outlined {
|
||||
font-size: 1.25rem;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.search-icon {
|
||||
left: 0.75rem;
|
||||
position: absolute;
|
||||
color: var(--quaternary-contrast);
|
||||
}
|
||||
|
||||
[ngComboboxInput] {
|
||||
width: 13rem;
|
||||
font-size: 1rem;
|
||||
border-radius: 3rem;
|
||||
padding: 0.75rem 0.5rem 0.75rem 2.5rem;
|
||||
color: var(--primary-contrast);
|
||||
outline-color: var(--primary);
|
||||
border: 1px solid var(--quinary-contrast);
|
||||
background-color: var(--page-background);
|
||||
}
|
||||
|
||||
[ngComboboxInput]::placeholder {
|
||||
color: var(--quaternary-contrast);
|
||||
}
|
||||
|
||||
[ngCombobox]:focus-within [ngComboboxInput] {
|
||||
outline: 2px solid var(--primary);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
[ngCombobox]:has([aria-expanded='false']) .popup {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.popup {
|
||||
width: 100%;
|
||||
margin-top: 8px;
|
||||
padding: 0.5rem;
|
||||
max-height: 11rem;
|
||||
border-radius: 2rem;
|
||||
background-color: var(--septenary-contrast);
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.no-results {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
[ngListbox] {
|
||||
gap: 2px;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
overflow: auto;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
[ngOption] {
|
||||
display: flex;
|
||||
cursor: pointer;
|
||||
align-items: center;
|
||||
margin: 1px;
|
||||
padding: 0 1rem;
|
||||
min-height: 3rem;
|
||||
border-radius: 3rem;
|
||||
}
|
||||
|
||||
[ngOption]:hover,
|
||||
[ngOption][data-active='true'] {
|
||||
background-color: color-mix(in srgb, var(--primary-contrast) 5%, transparent);
|
||||
}
|
||||
|
||||
[ngOption][data-active='true'] {
|
||||
outline-offset: -2px;
|
||||
outline: 2px solid var(--primary);
|
||||
}
|
||||
|
||||
[ngOption][aria-selected='true'] {
|
||||
color: var(--primary);
|
||||
background-color: color-mix(in srgb, var(--primary) 10%, transparent);
|
||||
}
|
||||
|
||||
[ngOption]:not([aria-selected='true']) .check-icon {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.option-label {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.check-icon {
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
<div ngCombobox filterMode="auto-select">
|
||||
<div #origin class="material-autocomplete">
|
||||
<span class="search-icon material-symbols-outlined">search</span>
|
||||
<input
|
||||
aria-label="Label dropdown"
|
||||
placeholder="Select a country"
|
||||
[(ngModel)]="query"
|
||||
ngComboboxInput
|
||||
/>
|
||||
</div>
|
||||
|
||||
<ng-template ngComboboxPopupContainer>
|
||||
<ng-template
|
||||
[cdkConnectedOverlay]="{origin, usePopover: 'inline', matchWidth: true}"
|
||||
[cdkConnectedOverlayOpen]="true"
|
||||
>
|
||||
<div class="popup">
|
||||
@if (countries().length === 0) {
|
||||
<div class="no-results">No results found</div>
|
||||
}
|
||||
|
||||
<div ngListbox>
|
||||
@for (country of countries(); track country) {
|
||||
<div ngOption [value]="country" [label]="country">
|
||||
<span class="option-label">{{country}}</span>
|
||||
<span class="check-icon material-symbols-outlined">check</span>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</ng-template>
|
||||
</ng-template>
|
||||
</div>
|
||||
|
|
@ -0,0 +1,261 @@
|
|||
import {Combobox, ComboboxInput, ComboboxPopupContainer} from '@angular/aria/combobox';
|
||||
import {Listbox, Option} from '@angular/aria/listbox';
|
||||
import {OverlayModule} from '@angular/cdk/overlay';
|
||||
import {
|
||||
afterRenderEffect,
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
computed,
|
||||
signal,
|
||||
viewChild,
|
||||
viewChildren,
|
||||
} from '@angular/core';
|
||||
import {FormsModule} from '@angular/forms';
|
||||
|
||||
@Component({
|
||||
selector: 'app-root',
|
||||
templateUrl: 'app.component.html',
|
||||
styleUrl: 'app.component.css',
|
||||
imports: [
|
||||
Combobox,
|
||||
ComboboxInput,
|
||||
ComboboxPopupContainer,
|
||||
Listbox,
|
||||
Option,
|
||||
OverlayModule,
|
||||
FormsModule,
|
||||
],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class App {
|
||||
/** The combobox listbox popup. */
|
||||
listbox = viewChild<Listbox<string>>(Listbox);
|
||||
|
||||
/** The options available in the listbox. */
|
||||
options = viewChildren<Option<string>>(Option);
|
||||
|
||||
/** A reference to the ng aria combobox. */
|
||||
combobox = viewChild<Combobox<string>>(Combobox);
|
||||
|
||||
/** The query string used to filter the list of countries. */
|
||||
query = signal('');
|
||||
|
||||
/** The list of countries filtered by the query. */
|
||||
countries = computed(() =>
|
||||
ALL_COUNTRIES.filter((country) => country.toLowerCase().startsWith(this.query().toLowerCase())),
|
||||
);
|
||||
|
||||
constructor() {
|
||||
// Scrolls to the active item when the active option changes.
|
||||
// The slight delay here is to ensure animations are done before scrolling.
|
||||
afterRenderEffect(() => {
|
||||
const option = this.options().find((opt) => opt.active());
|
||||
setTimeout(() => option?.element.scrollIntoView({block: 'nearest'}), 50);
|
||||
});
|
||||
|
||||
// Resets the listbox scroll position when the combobox is closed.
|
||||
afterRenderEffect(() => {
|
||||
if (!this.combobox()?.expanded()) {
|
||||
setTimeout(() => this.listbox()?.element.scrollTo(0, 0), 150);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const ALL_COUNTRIES = [
|
||||
'Afghanistan',
|
||||
'Albania',
|
||||
'Algeria',
|
||||
'Andorra',
|
||||
'Angola',
|
||||
'Antigua and Barbuda',
|
||||
'Argentina',
|
||||
'Armenia',
|
||||
'Australia',
|
||||
'Austria',
|
||||
'Azerbaijan',
|
||||
'Bahamas',
|
||||
'Bahrain',
|
||||
'Bangladesh',
|
||||
'Barbados',
|
||||
'Belarus',
|
||||
'Belgium',
|
||||
'Belize',
|
||||
'Benin',
|
||||
'Bhutan',
|
||||
'Bolivia',
|
||||
'Bosnia and Herzegovina',
|
||||
'Botswana',
|
||||
'Brazil',
|
||||
'Brunei',
|
||||
'Bulgaria',
|
||||
'Burkina Faso',
|
||||
'Burundi',
|
||||
'Cabo Verde',
|
||||
'Cambodia',
|
||||
'Cameroon',
|
||||
'Canada',
|
||||
'Central African Republic',
|
||||
'Chad',
|
||||
'Chile',
|
||||
'China',
|
||||
'Colombia',
|
||||
'Comoros',
|
||||
'Congo (Congo-Brazzaville)',
|
||||
'Costa Rica',
|
||||
"Côte d'Ivoire",
|
||||
'Croatia',
|
||||
'Cuba',
|
||||
'Cyprus',
|
||||
'Czechia (Czech Republic)',
|
||||
'Democratic Republic of the Congo',
|
||||
'Denmark',
|
||||
'Djibouti',
|
||||
'Dominica',
|
||||
'Dominican Republic',
|
||||
'Ecuador',
|
||||
'Egypt',
|
||||
'El Salvador',
|
||||
'Equatorial Guinea',
|
||||
'Eritrea',
|
||||
'Estonia',
|
||||
'Eswatini (fmr. ""Swaziland"")',
|
||||
'Ethiopia',
|
||||
'Fiji',
|
||||
'Finland',
|
||||
'France',
|
||||
'Gabon',
|
||||
'Gambia',
|
||||
'Georgia',
|
||||
'Germany',
|
||||
'Ghana',
|
||||
'Greece',
|
||||
'Grenada',
|
||||
'Guatemala',
|
||||
'Guinea',
|
||||
'Guinea-Bissau',
|
||||
'Guyana',
|
||||
'Haiti',
|
||||
'Holy See',
|
||||
'Honduras',
|
||||
'Hungary',
|
||||
'Iceland',
|
||||
'India',
|
||||
'Indonesia',
|
||||
'Iran',
|
||||
'Iraq',
|
||||
'Ireland',
|
||||
'Israel',
|
||||
'Italy',
|
||||
'Jamaica',
|
||||
'Japan',
|
||||
'Jordan',
|
||||
'Kazakhstan',
|
||||
'Kenya',
|
||||
'Kiribati',
|
||||
'Kuwait',
|
||||
'Kyrgyzstan',
|
||||
'Laos',
|
||||
'Latvia',
|
||||
'Lebanon',
|
||||
'Lesotho',
|
||||
'Liberia',
|
||||
'Libya',
|
||||
'Liechtenstein',
|
||||
'Lithuania',
|
||||
'Luxembourg',
|
||||
'Madagascar',
|
||||
'Malawi',
|
||||
'Malaysia',
|
||||
'Maldives',
|
||||
'Mali',
|
||||
'Malta',
|
||||
'Marshall Islands',
|
||||
'Mauritania',
|
||||
'Mauritius',
|
||||
'Mexico',
|
||||
'Micronesia',
|
||||
'Moldova',
|
||||
'Monaco',
|
||||
'Mongolia',
|
||||
'Montenegro',
|
||||
'Morocco',
|
||||
'Mozambique',
|
||||
'Myanmar (formerly Burma)',
|
||||
'Namibia',
|
||||
'Nauru',
|
||||
'Nepal',
|
||||
'Netherlands',
|
||||
'New Zealand',
|
||||
'Nicaragua',
|
||||
'Niger',
|
||||
'Nigeria',
|
||||
'North Korea',
|
||||
'North Macedonia',
|
||||
'Norway',
|
||||
'Oman',
|
||||
'Pakistan',
|
||||
'Palau',
|
||||
'Palestine State',
|
||||
'Panama',
|
||||
'Papua New Guinea',
|
||||
'Paraguay',
|
||||
'Peru',
|
||||
'Philippines',
|
||||
'Poland',
|
||||
'Portugal',
|
||||
'Qatar',
|
||||
'Romania',
|
||||
'Russia',
|
||||
'Rwanda',
|
||||
'Saint Kitts and Nevis',
|
||||
'Saint Lucia',
|
||||
'Saint Vincent and the Grenadines',
|
||||
'Samoa',
|
||||
'San Marino',
|
||||
'Sao Tome and Principe',
|
||||
'Saudi Arabia',
|
||||
'Senegal',
|
||||
'Serbia',
|
||||
'Seychelles',
|
||||
'Sierra Leone',
|
||||
'Singapore',
|
||||
'Slovakia',
|
||||
'Slovenia',
|
||||
'Solomon Islands',
|
||||
'Somalia',
|
||||
'South Africa',
|
||||
'South Korea',
|
||||
'South Sudan',
|
||||
'Spain',
|
||||
'Sri Lanka',
|
||||
'Sudan',
|
||||
'Suriname',
|
||||
'Sweden',
|
||||
'Switzerland',
|
||||
'Syria',
|
||||
'Tajikistan',
|
||||
'Tanzania',
|
||||
'Thailand',
|
||||
'Timor-Leste',
|
||||
'Togo',
|
||||
'Tonga',
|
||||
'Trinidad and Tobago',
|
||||
'Tunisia',
|
||||
'Turkey',
|
||||
'Turkmenistan',
|
||||
'Tuvalu',
|
||||
'Uganda',
|
||||
'Ukraine',
|
||||
'United Arab Emirates',
|
||||
'United Kingdom',
|
||||
'United States of America',
|
||||
'Uruguay',
|
||||
'Uzbekistan',
|
||||
'Vanuatu',
|
||||
'Venezuela',
|
||||
'Vietnam',
|
||||
'Yemen',
|
||||
'Zambia',
|
||||
'Zimbabwe',
|
||||
];
|
||||
|
|
@ -0,0 +1,130 @@
|
|||
@import url('https://fonts.googleapis.com/icon?family=Material+Symbols+Outlined');
|
||||
@import url('https://fonts.googleapis.com/css2?family=Press+Start+2P&display=swap');
|
||||
|
||||
:host {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
font-size: 0.6rem;
|
||||
font-family: 'Press Start 2P';
|
||||
|
||||
--retro-button-color: #fff;
|
||||
--retro-shadow-light: color-mix(in srgb, var(--retro-button-color) 90%, #fff);
|
||||
--retro-shadow-dark: color-mix(in srgb, var(--retro-button-color) 90%, #000);
|
||||
--retro-elevated-shadow:
|
||||
inset 4px 4px 0px 0px var(--retro-shadow-light),
|
||||
inset -4px -4px 0px 0px var(--retro-shadow-dark), 4px 0px 0px 0px var(--gray-700),
|
||||
0px 4px 0px 0px var(--gray-700), -4px 0px 0px 0px var(--gray-700),
|
||||
0px -4px 0px 0px var(--gray-700);
|
||||
--retro-flat-shadow:
|
||||
4px 0px 0px 0px var(--gray-700), 0px 4px 0px 0px var(--gray-700),
|
||||
-4px 0px 0px 0px var(--gray-700), 0px -4px 0px 0px var(--gray-700);
|
||||
--retro-pressed-shadow:
|
||||
inset 4px 4px 0px 0px var(--retro-shadow-dark),
|
||||
inset -4px -4px 0px 0px var(--retro-shadow-light), 4px 0px 0px 0px var(--gray-700),
|
||||
0px 4px 0px 0px var(--gray-700), -4px 0px 0px 0px var(--gray-700),
|
||||
0px -4px 0px 0px var(--gray-700), 0px 0px 0px 0px var(--gray-700);
|
||||
}
|
||||
|
||||
.retro-autocomplete {
|
||||
display: flex;
|
||||
position: relative;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.material-symbols-outlined {
|
||||
font-size: 1.25rem;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.search-icon {
|
||||
left: 0.75rem;
|
||||
position: absolute;
|
||||
color: #000;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
[ngComboboxInput] {
|
||||
width: 15rem;
|
||||
font-size: 0.6rem;
|
||||
border-radius: 0;
|
||||
font-family: 'Press Start 2P';
|
||||
word-spacing: -5px;
|
||||
padding: 0.75rem 0.5rem 0.75rem 2.5rem;
|
||||
color: #000;
|
||||
border: none;
|
||||
box-shadow: var(--retro-flat-shadow);
|
||||
background-color: var(--retro-button-color);
|
||||
}
|
||||
|
||||
[ngComboboxInput]::placeholder {
|
||||
color: #000;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
[ngComboboxInput]:focus {
|
||||
outline: none;
|
||||
transform: translate(1px, 1px);
|
||||
box-shadow: var(--retro-pressed-shadow);
|
||||
}
|
||||
|
||||
[ngCombobox]:has([aria-expanded='false']) .popup {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.popup {
|
||||
width: 100%;
|
||||
margin-top: 20px;
|
||||
padding: 0.5rem;
|
||||
max-height: 11rem;
|
||||
border-radius: 0;
|
||||
background-color: var(--septenary-contrast);
|
||||
box-shadow: var(--retro-flat-shadow);
|
||||
}
|
||||
|
||||
.no-results {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
[ngListbox] {
|
||||
gap: 2px;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
overflow: auto;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
[ngOption] {
|
||||
display: flex;
|
||||
cursor: pointer;
|
||||
align-items: center;
|
||||
margin: 1px;
|
||||
padding: 0 1rem;
|
||||
min-height: 2.25rem;
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
[ngOption]:hover {
|
||||
background-color: color-mix(in srgb, var(--primary-contrast) 5%, transparent);
|
||||
}
|
||||
|
||||
[ngOption][data-active='true'] {
|
||||
outline-offset: -2px;
|
||||
outline: 2px dashed var(--hot-pink);
|
||||
}
|
||||
|
||||
[ngOption][aria-selected='true'] {
|
||||
color: var(--hot-pink);
|
||||
background-color: color-mix(in srgb, var(--hot-pink) 5%, transparent);
|
||||
}
|
||||
|
||||
[ngOption]:not([aria-selected='true']) .check-icon {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.option-label {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.check-icon {
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
<div ngCombobox filterMode="auto-select">
|
||||
<div #origin class="retro-autocomplete">
|
||||
<span class="search-icon material-symbols-outlined">search</span>
|
||||
<input
|
||||
aria-label="Label dropdown"
|
||||
placeholder="Select a country"
|
||||
[(ngModel)]="query"
|
||||
ngComboboxInput
|
||||
/>
|
||||
</div>
|
||||
|
||||
<ng-template ngComboboxPopupContainer>
|
||||
<ng-template
|
||||
[cdkConnectedOverlay]="{origin, usePopover: 'inline', matchWidth: true}"
|
||||
[cdkConnectedOverlayOpen]="true"
|
||||
>
|
||||
<div class="popup">
|
||||
@if (countries().length === 0) {
|
||||
<div class="no-results">No results found</div>
|
||||
}
|
||||
|
||||
<div ngListbox>
|
||||
@for (country of countries(); track country) {
|
||||
<div ngOption [value]="country" [label]="country">
|
||||
<span class="option-label">{{country}}</span>
|
||||
<span class="check-icon material-symbols-outlined">check</span>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</ng-template>
|
||||
</ng-template>
|
||||
</div>
|
||||
|
|
@ -0,0 +1,261 @@
|
|||
import {Combobox, ComboboxInput, ComboboxPopupContainer} from '@angular/aria/combobox';
|
||||
import {Listbox, Option} from '@angular/aria/listbox';
|
||||
import {OverlayModule} from '@angular/cdk/overlay';
|
||||
import {
|
||||
afterRenderEffect,
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
computed,
|
||||
signal,
|
||||
viewChild,
|
||||
viewChildren,
|
||||
} from '@angular/core';
|
||||
import {FormsModule} from '@angular/forms';
|
||||
|
||||
@Component({
|
||||
selector: 'app-root',
|
||||
templateUrl: 'app.component.html',
|
||||
styleUrl: 'app.component.css',
|
||||
imports: [
|
||||
Combobox,
|
||||
ComboboxInput,
|
||||
ComboboxPopupContainer,
|
||||
Listbox,
|
||||
Option,
|
||||
OverlayModule,
|
||||
FormsModule,
|
||||
],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class App {
|
||||
/** The combobox listbox popup. */
|
||||
listbox = viewChild<Listbox<string>>(Listbox);
|
||||
|
||||
/** The options available in the listbox. */
|
||||
options = viewChildren<Option<string>>(Option);
|
||||
|
||||
/** A reference to the ng aria combobox. */
|
||||
combobox = viewChild<Combobox<string>>(Combobox);
|
||||
|
||||
/** The query string used to filter the list of countries. */
|
||||
query = signal('');
|
||||
|
||||
/** The list of countries filtered by the query. */
|
||||
countries = computed(() =>
|
||||
ALL_COUNTRIES.filter((country) => country.toLowerCase().startsWith(this.query().toLowerCase())),
|
||||
);
|
||||
|
||||
constructor() {
|
||||
// Scrolls to the active item when the active option changes.
|
||||
// The slight delay here is to ensure animations are done before scrolling.
|
||||
afterRenderEffect(() => {
|
||||
const option = this.options().find((opt) => opt.active());
|
||||
setTimeout(() => option?.element.scrollIntoView({block: 'nearest'}), 50);
|
||||
});
|
||||
|
||||
// Resets the listbox scroll position when the combobox is closed.
|
||||
afterRenderEffect(() => {
|
||||
if (!this.combobox()?.expanded()) {
|
||||
setTimeout(() => this.listbox()?.element.scrollTo(0, 0), 150);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const ALL_COUNTRIES = [
|
||||
'Afghanistan',
|
||||
'Albania',
|
||||
'Algeria',
|
||||
'Andorra',
|
||||
'Angola',
|
||||
'Antigua and Barbuda',
|
||||
'Argentina',
|
||||
'Armenia',
|
||||
'Australia',
|
||||
'Austria',
|
||||
'Azerbaijan',
|
||||
'Bahamas',
|
||||
'Bahrain',
|
||||
'Bangladesh',
|
||||
'Barbados',
|
||||
'Belarus',
|
||||
'Belgium',
|
||||
'Belize',
|
||||
'Benin',
|
||||
'Bhutan',
|
||||
'Bolivia',
|
||||
'Bosnia and Herzegovina',
|
||||
'Botswana',
|
||||
'Brazil',
|
||||
'Brunei',
|
||||
'Bulgaria',
|
||||
'Burkina Faso',
|
||||
'Burundi',
|
||||
'Cabo Verde',
|
||||
'Cambodia',
|
||||
'Cameroon',
|
||||
'Canada',
|
||||
'Central African Republic',
|
||||
'Chad',
|
||||
'Chile',
|
||||
'China',
|
||||
'Colombia',
|
||||
'Comoros',
|
||||
'Congo (Congo-Brazzaville)',
|
||||
'Costa Rica',
|
||||
"Côte d'Ivoire",
|
||||
'Croatia',
|
||||
'Cuba',
|
||||
'Cyprus',
|
||||
'Czechia (Czech Republic)',
|
||||
'Democratic Republic of the Congo',
|
||||
'Denmark',
|
||||
'Djibouti',
|
||||
'Dominica',
|
||||
'Dominican Republic',
|
||||
'Ecuador',
|
||||
'Egypt',
|
||||
'El Salvador',
|
||||
'Equatorial Guinea',
|
||||
'Eritrea',
|
||||
'Estonia',
|
||||
'Eswatini (fmr. ""Swaziland"")',
|
||||
'Ethiopia',
|
||||
'Fiji',
|
||||
'Finland',
|
||||
'France',
|
||||
'Gabon',
|
||||
'Gambia',
|
||||
'Georgia',
|
||||
'Germany',
|
||||
'Ghana',
|
||||
'Greece',
|
||||
'Grenada',
|
||||
'Guatemala',
|
||||
'Guinea',
|
||||
'Guinea-Bissau',
|
||||
'Guyana',
|
||||
'Haiti',
|
||||
'Holy See',
|
||||
'Honduras',
|
||||
'Hungary',
|
||||
'Iceland',
|
||||
'India',
|
||||
'Indonesia',
|
||||
'Iran',
|
||||
'Iraq',
|
||||
'Ireland',
|
||||
'Israel',
|
||||
'Italy',
|
||||
'Jamaica',
|
||||
'Japan',
|
||||
'Jordan',
|
||||
'Kazakhstan',
|
||||
'Kenya',
|
||||
'Kiribati',
|
||||
'Kuwait',
|
||||
'Kyrgyzstan',
|
||||
'Laos',
|
||||
'Latvia',
|
||||
'Lebanon',
|
||||
'Lesotho',
|
||||
'Liberia',
|
||||
'Libya',
|
||||
'Liechtenstein',
|
||||
'Lithuania',
|
||||
'Luxembourg',
|
||||
'Madagascar',
|
||||
'Malawi',
|
||||
'Malaysia',
|
||||
'Maldives',
|
||||
'Mali',
|
||||
'Malta',
|
||||
'Marshall Islands',
|
||||
'Mauritania',
|
||||
'Mauritius',
|
||||
'Mexico',
|
||||
'Micronesia',
|
||||
'Moldova',
|
||||
'Monaco',
|
||||
'Mongolia',
|
||||
'Montenegro',
|
||||
'Morocco',
|
||||
'Mozambique',
|
||||
'Myanmar (formerly Burma)',
|
||||
'Namibia',
|
||||
'Nauru',
|
||||
'Nepal',
|
||||
'Netherlands',
|
||||
'New Zealand',
|
||||
'Nicaragua',
|
||||
'Niger',
|
||||
'Nigeria',
|
||||
'North Korea',
|
||||
'North Macedonia',
|
||||
'Norway',
|
||||
'Oman',
|
||||
'Pakistan',
|
||||
'Palau',
|
||||
'Palestine State',
|
||||
'Panama',
|
||||
'Papua New Guinea',
|
||||
'Paraguay',
|
||||
'Peru',
|
||||
'Philippines',
|
||||
'Poland',
|
||||
'Portugal',
|
||||
'Qatar',
|
||||
'Romania',
|
||||
'Russia',
|
||||
'Rwanda',
|
||||
'Saint Kitts and Nevis',
|
||||
'Saint Lucia',
|
||||
'Saint Vincent and the Grenadines',
|
||||
'Samoa',
|
||||
'San Marino',
|
||||
'Sao Tome and Principe',
|
||||
'Saudi Arabia',
|
||||
'Senegal',
|
||||
'Serbia',
|
||||
'Seychelles',
|
||||
'Sierra Leone',
|
||||
'Singapore',
|
||||
'Slovakia',
|
||||
'Slovenia',
|
||||
'Solomon Islands',
|
||||
'Somalia',
|
||||
'South Africa',
|
||||
'South Korea',
|
||||
'South Sudan',
|
||||
'Spain',
|
||||
'Sri Lanka',
|
||||
'Sudan',
|
||||
'Suriname',
|
||||
'Sweden',
|
||||
'Switzerland',
|
||||
'Syria',
|
||||
'Tajikistan',
|
||||
'Tanzania',
|
||||
'Thailand',
|
||||
'Timor-Leste',
|
||||
'Togo',
|
||||
'Tonga',
|
||||
'Trinidad and Tobago',
|
||||
'Tunisia',
|
||||
'Turkey',
|
||||
'Turkmenistan',
|
||||
'Tuvalu',
|
||||
'Uganda',
|
||||
'Ukraine',
|
||||
'United Arab Emirates',
|
||||
'United Kingdom',
|
||||
'United States of America',
|
||||
'Uruguay',
|
||||
'Uzbekistan',
|
||||
'Vanuatu',
|
||||
'Venezuela',
|
||||
'Vietnam',
|
||||
'Yemen',
|
||||
'Zambia',
|
||||
'Zimbabwe',
|
||||
];
|
||||
|
|
@ -1,89 +1,101 @@
|
|||
html {
|
||||
@import url('https://fonts.googleapis.com/icon?family=Material+Symbols+Outlined');
|
||||
|
||||
:host {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
font-family: var(--inter-font);
|
||||
}
|
||||
|
||||
.combobox-container {
|
||||
max-width: 400px;
|
||||
margin: 20px;
|
||||
}
|
||||
|
||||
label {
|
||||
display: block;
|
||||
margin-bottom: 8px;
|
||||
font-weight: 500;
|
||||
color: #e0e0e0;
|
||||
}
|
||||
|
||||
.input-container {
|
||||
.autocomplete {
|
||||
display: flex;
|
||||
position: relative;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.combobox-input {
|
||||
.material-symbols-outlined {
|
||||
font-size: 1.25rem;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.search-icon {
|
||||
left: 0.75rem;
|
||||
position: absolute;
|
||||
color: var(--quaternary-contrast);
|
||||
}
|
||||
|
||||
[ngComboboxInput] {
|
||||
width: 13rem;
|
||||
font-size: 1rem;
|
||||
border-radius: 0.25rem;
|
||||
padding: 0.75rem 0.5rem 0.75rem 2.5rem;
|
||||
color: var(--primary-contrast);
|
||||
outline-color: var(--hot-pink);
|
||||
border: 1px solid var(--quinary-contrast);
|
||||
background-color: var(--page-background);
|
||||
}
|
||||
|
||||
[ngComboboxInput]::placeholder {
|
||||
color: var(--quaternary-contrast);
|
||||
}
|
||||
|
||||
[ngCombobox]:has([aria-expanded='false']) .popup {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.popup {
|
||||
width: 100%;
|
||||
padding: 10px 12px;
|
||||
border: 1px solid #404040;
|
||||
border-radius: 4px;
|
||||
font-size: 16px;
|
||||
box-sizing: border-box;
|
||||
background-color: #1a1a1a;
|
||||
color: #e0e0e0;
|
||||
margin-top: 8px;
|
||||
padding: 0.5rem;
|
||||
max-height: 11rem;
|
||||
border-radius: 0.5rem;
|
||||
background-color: var(--septenary-contrast);
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.combobox-input::placeholder {
|
||||
color: #888;
|
||||
.no-results {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.combobox-input:focus {
|
||||
outline: none;
|
||||
border-color: #4a9eff;
|
||||
background-color: #1f1f1f;
|
||||
[ngListbox] {
|
||||
gap: 2px;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
overflow: auto;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.popover {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
border: 1px solid #404040;
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.5);
|
||||
background: #1a1a1a;
|
||||
max-height: 300px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.listbox {
|
||||
padding: 4px 0;
|
||||
}
|
||||
|
||||
.option {
|
||||
padding: 10px 12px;
|
||||
[ngOption] {
|
||||
display: flex;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
color: #e0e0e0;
|
||||
align-items: center;
|
||||
margin: 1px;
|
||||
padding: 0 1rem;
|
||||
min-height: 2.25rem;
|
||||
border-radius: 0.5rem;
|
||||
}
|
||||
|
||||
.option:hover {
|
||||
background-color: #2a2a2a;
|
||||
[ngOption]:hover {
|
||||
background-color: color-mix(in srgb, var(--primary-contrast) 5%, transparent);
|
||||
}
|
||||
|
||||
.option[data-active] {
|
||||
background-color: #2d4a6e;
|
||||
color: #ffffff;
|
||||
[ngOption][data-active='true'] {
|
||||
outline-offset: -2px;
|
||||
outline: 2px solid var(--hot-pink);
|
||||
}
|
||||
|
||||
.option[aria-selected='true'] {
|
||||
background-color: #4a9eff;
|
||||
color: #000000;
|
||||
[ngOption][aria-selected='true'] {
|
||||
color: var(--hot-pink);
|
||||
background-color: color-mix(in srgb, var(--hot-pink) 5%, transparent);
|
||||
}
|
||||
|
||||
.info {
|
||||
margin: 20px;
|
||||
padding: 16px;
|
||||
background-color: #1f1f1f;
|
||||
border-radius: 4px;
|
||||
border-left: 4px solid #4a9eff;
|
||||
color: #e0e0e0;
|
||||
[ngOption]:not([aria-selected='true']) .check-icon {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.info p {
|
||||
margin: 8px 0;
|
||||
.option-label {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.check-icon {
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,31 +1,33 @@
|
|||
<div ngCombobox #combobox="ngCombobox" filterMode="highlight" class="combobox-container">
|
||||
<label for="state-input">Select a state:</label>
|
||||
<div class="input-container">
|
||||
<div ngCombobox filterMode="highlight">
|
||||
<div #origin class="autocomplete">
|
||||
<span class="search-icon material-symbols-outlined">search</span>
|
||||
<input
|
||||
id="state-input"
|
||||
aria-label="Label dropdown"
|
||||
placeholder="Select a country"
|
||||
[(ngModel)]="query"
|
||||
ngComboboxInput
|
||||
placeholder="Search states..."
|
||||
[(value)]="searchString"
|
||||
class="combobox-input"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div popover="manual" #popover class="popover">
|
||||
<ng-template ngComboboxPopupContainer>
|
||||
<div ngListbox class="listbox">
|
||||
@for (option of options(); track option) {
|
||||
<div class="option" ngOption [value]="option" [label]="option">
|
||||
<span>{{ option }}</span>
|
||||
</div>
|
||||
<ng-template ngComboboxPopupContainer>
|
||||
<ng-template
|
||||
[cdkConnectedOverlay]="{origin, usePopover: 'inline', matchWidth: true}"
|
||||
[cdkConnectedOverlayOpen]="true"
|
||||
>
|
||||
<div class="popup">
|
||||
@if (countries().length === 0) {
|
||||
<div class="no-results">No results found</div>
|
||||
}
|
||||
|
||||
<div ngListbox>
|
||||
@for (country of countries(); track country) {
|
||||
<div ngOption [value]="country" [label]="country">
|
||||
<span class="option-label">{{country}}</span>
|
||||
<span class="check-icon material-symbols-outlined">check</span>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</ng-template>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="info">
|
||||
<p><strong>Highlight mode:</strong> Options are highlighted as you navigate with arrow keys, but the input only updates when you press Enter or click.</p>
|
||||
@if (searchString()) {
|
||||
<p>Current value: <strong>{{ searchString() }}</strong></p>
|
||||
}
|
||||
</ng-template>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,108 +1,261 @@
|
|||
import {Combobox, ComboboxInput, ComboboxPopupContainer} from '@angular/aria/combobox';
|
||||
import {Listbox, Option} from '@angular/aria/listbox';
|
||||
import {OverlayModule} from '@angular/cdk/overlay';
|
||||
import {
|
||||
afterRenderEffect,
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
computed,
|
||||
ElementRef,
|
||||
signal,
|
||||
viewChild,
|
||||
viewChildren,
|
||||
} from '@angular/core';
|
||||
import {FormsModule} from '@angular/forms';
|
||||
|
||||
@Component({
|
||||
selector: 'app-root',
|
||||
templateUrl: 'app.component.html',
|
||||
styleUrl: 'app.component.css',
|
||||
imports: [Combobox, ComboboxInput, ComboboxPopupContainer, Listbox, Option],
|
||||
imports: [
|
||||
Combobox,
|
||||
ComboboxInput,
|
||||
ComboboxPopupContainer,
|
||||
Listbox,
|
||||
Option,
|
||||
OverlayModule,
|
||||
FormsModule,
|
||||
],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class App {
|
||||
popover = viewChild<ElementRef>('popover');
|
||||
listbox = viewChild<Listbox<any>>(Listbox);
|
||||
combobox = viewChild<Combobox<any>>(Combobox);
|
||||
/** The combobox listbox popup. */
|
||||
listbox = viewChild<Listbox<string>>(Listbox);
|
||||
|
||||
searchString = signal('');
|
||||
/** The options available in the listbox. */
|
||||
options = viewChildren<Option<string>>(Option);
|
||||
|
||||
options = computed(() =>
|
||||
states.filter((state) => state.toLowerCase().startsWith(this.searchString().toLowerCase())),
|
||||
/** A reference to the ng aria combobox. */
|
||||
combobox = viewChild<Combobox<string>>(Combobox);
|
||||
|
||||
/** The query string used to filter the list of countries. */
|
||||
query = signal('');
|
||||
|
||||
/** The list of countries filtered by the query. */
|
||||
countries = computed(() =>
|
||||
ALL_COUNTRIES.filter((country) => country.toLowerCase().startsWith(this.query().toLowerCase())),
|
||||
);
|
||||
|
||||
constructor() {
|
||||
// Scrolls to the active item when the active option changes.
|
||||
// The slight delay here is to ensure animations are done before scrolling.
|
||||
afterRenderEffect(() => {
|
||||
const popover = this.popover()!;
|
||||
const combobox = this.combobox()!;
|
||||
combobox.expanded() ? this.showPopover() : popover.nativeElement.hidePopover();
|
||||
this.listbox()?.scrollActiveItemIntoView();
|
||||
const option = this.options().find((opt) => opt.active());
|
||||
setTimeout(() => option?.element.scrollIntoView({block: 'nearest'}), 50);
|
||||
});
|
||||
}
|
||||
|
||||
showPopover() {
|
||||
const popover = this.popover()!;
|
||||
const combobox = this.combobox()!;
|
||||
|
||||
const comboboxRect = combobox.inputElement()?.getBoundingClientRect();
|
||||
const popoverEl = popover.nativeElement;
|
||||
|
||||
if (comboboxRect) {
|
||||
popoverEl.style.width = `${comboboxRect.width}px`;
|
||||
popoverEl.style.top = `${comboboxRect.bottom + 4}px`;
|
||||
popoverEl.style.left = `${comboboxRect.left - 1}px`;
|
||||
}
|
||||
|
||||
popover.nativeElement.showPopover();
|
||||
// Resets the listbox scroll position when the combobox is closed.
|
||||
afterRenderEffect(() => {
|
||||
if (!this.combobox()?.expanded()) {
|
||||
setTimeout(() => this.listbox()?.element.scrollTo(0, 0), 150);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const states = [
|
||||
'Alabama',
|
||||
'Alaska',
|
||||
'Arizona',
|
||||
'Arkansas',
|
||||
'California',
|
||||
'Colorado',
|
||||
'Connecticut',
|
||||
'Delaware',
|
||||
'Florida',
|
||||
const ALL_COUNTRIES = [
|
||||
'Afghanistan',
|
||||
'Albania',
|
||||
'Algeria',
|
||||
'Andorra',
|
||||
'Angola',
|
||||
'Antigua and Barbuda',
|
||||
'Argentina',
|
||||
'Armenia',
|
||||
'Australia',
|
||||
'Austria',
|
||||
'Azerbaijan',
|
||||
'Bahamas',
|
||||
'Bahrain',
|
||||
'Bangladesh',
|
||||
'Barbados',
|
||||
'Belarus',
|
||||
'Belgium',
|
||||
'Belize',
|
||||
'Benin',
|
||||
'Bhutan',
|
||||
'Bolivia',
|
||||
'Bosnia and Herzegovina',
|
||||
'Botswana',
|
||||
'Brazil',
|
||||
'Brunei',
|
||||
'Bulgaria',
|
||||
'Burkina Faso',
|
||||
'Burundi',
|
||||
'Cabo Verde',
|
||||
'Cambodia',
|
||||
'Cameroon',
|
||||
'Canada',
|
||||
'Central African Republic',
|
||||
'Chad',
|
||||
'Chile',
|
||||
'China',
|
||||
'Colombia',
|
||||
'Comoros',
|
||||
'Congo (Congo-Brazzaville)',
|
||||
'Costa Rica',
|
||||
"Côte d'Ivoire",
|
||||
'Croatia',
|
||||
'Cuba',
|
||||
'Cyprus',
|
||||
'Czechia (Czech Republic)',
|
||||
'Democratic Republic of the Congo',
|
||||
'Denmark',
|
||||
'Djibouti',
|
||||
'Dominica',
|
||||
'Dominican Republic',
|
||||
'Ecuador',
|
||||
'Egypt',
|
||||
'El Salvador',
|
||||
'Equatorial Guinea',
|
||||
'Eritrea',
|
||||
'Estonia',
|
||||
'Eswatini (fmr. ""Swaziland"")',
|
||||
'Ethiopia',
|
||||
'Fiji',
|
||||
'Finland',
|
||||
'France',
|
||||
'Gabon',
|
||||
'Gambia',
|
||||
'Georgia',
|
||||
'Hawaii',
|
||||
'Idaho',
|
||||
'Illinois',
|
||||
'Indiana',
|
||||
'Iowa',
|
||||
'Kansas',
|
||||
'Kentucky',
|
||||
'Louisiana',
|
||||
'Maine',
|
||||
'Maryland',
|
||||
'Massachusetts',
|
||||
'Michigan',
|
||||
'Minnesota',
|
||||
'Mississippi',
|
||||
'Missouri',
|
||||
'Montana',
|
||||
'Nebraska',
|
||||
'Nevada',
|
||||
'New Hampshire',
|
||||
'New Jersey',
|
||||
'New Mexico',
|
||||
'New York',
|
||||
'North Carolina',
|
||||
'North Dakota',
|
||||
'Ohio',
|
||||
'Oklahoma',
|
||||
'Oregon',
|
||||
'Pennsylvania',
|
||||
'Rhode Island',
|
||||
'South Carolina',
|
||||
'South Dakota',
|
||||
'Tennessee',
|
||||
'Texas',
|
||||
'Utah',
|
||||
'Vermont',
|
||||
'Virginia',
|
||||
'Washington',
|
||||
'West Virginia',
|
||||
'Wisconsin',
|
||||
'Wyoming',
|
||||
'Germany',
|
||||
'Ghana',
|
||||
'Greece',
|
||||
'Grenada',
|
||||
'Guatemala',
|
||||
'Guinea',
|
||||
'Guinea-Bissau',
|
||||
'Guyana',
|
||||
'Haiti',
|
||||
'Holy See',
|
||||
'Honduras',
|
||||
'Hungary',
|
||||
'Iceland',
|
||||
'India',
|
||||
'Indonesia',
|
||||
'Iran',
|
||||
'Iraq',
|
||||
'Ireland',
|
||||
'Israel',
|
||||
'Italy',
|
||||
'Jamaica',
|
||||
'Japan',
|
||||
'Jordan',
|
||||
'Kazakhstan',
|
||||
'Kenya',
|
||||
'Kiribati',
|
||||
'Kuwait',
|
||||
'Kyrgyzstan',
|
||||
'Laos',
|
||||
'Latvia',
|
||||
'Lebanon',
|
||||
'Lesotho',
|
||||
'Liberia',
|
||||
'Libya',
|
||||
'Liechtenstein',
|
||||
'Lithuania',
|
||||
'Luxembourg',
|
||||
'Madagascar',
|
||||
'Malawi',
|
||||
'Malaysia',
|
||||
'Maldives',
|
||||
'Mali',
|
||||
'Malta',
|
||||
'Marshall Islands',
|
||||
'Mauritania',
|
||||
'Mauritius',
|
||||
'Mexico',
|
||||
'Micronesia',
|
||||
'Moldova',
|
||||
'Monaco',
|
||||
'Mongolia',
|
||||
'Montenegro',
|
||||
'Morocco',
|
||||
'Mozambique',
|
||||
'Myanmar (formerly Burma)',
|
||||
'Namibia',
|
||||
'Nauru',
|
||||
'Nepal',
|
||||
'Netherlands',
|
||||
'New Zealand',
|
||||
'Nicaragua',
|
||||
'Niger',
|
||||
'Nigeria',
|
||||
'North Korea',
|
||||
'North Macedonia',
|
||||
'Norway',
|
||||
'Oman',
|
||||
'Pakistan',
|
||||
'Palau',
|
||||
'Palestine State',
|
||||
'Panama',
|
||||
'Papua New Guinea',
|
||||
'Paraguay',
|
||||
'Peru',
|
||||
'Philippines',
|
||||
'Poland',
|
||||
'Portugal',
|
||||
'Qatar',
|
||||
'Romania',
|
||||
'Russia',
|
||||
'Rwanda',
|
||||
'Saint Kitts and Nevis',
|
||||
'Saint Lucia',
|
||||
'Saint Vincent and the Grenadines',
|
||||
'Samoa',
|
||||
'San Marino',
|
||||
'Sao Tome and Principe',
|
||||
'Saudi Arabia',
|
||||
'Senegal',
|
||||
'Serbia',
|
||||
'Seychelles',
|
||||
'Sierra Leone',
|
||||
'Singapore',
|
||||
'Slovakia',
|
||||
'Slovenia',
|
||||
'Solomon Islands',
|
||||
'Somalia',
|
||||
'South Africa',
|
||||
'South Korea',
|
||||
'South Sudan',
|
||||
'Spain',
|
||||
'Sri Lanka',
|
||||
'Sudan',
|
||||
'Suriname',
|
||||
'Sweden',
|
||||
'Switzerland',
|
||||
'Syria',
|
||||
'Tajikistan',
|
||||
'Tanzania',
|
||||
'Thailand',
|
||||
'Timor-Leste',
|
||||
'Togo',
|
||||
'Tonga',
|
||||
'Trinidad and Tobago',
|
||||
'Tunisia',
|
||||
'Turkey',
|
||||
'Turkmenistan',
|
||||
'Tuvalu',
|
||||
'Uganda',
|
||||
'Ukraine',
|
||||
'United Arab Emirates',
|
||||
'United Kingdom',
|
||||
'United States of America',
|
||||
'Uruguay',
|
||||
'Uzbekistan',
|
||||
'Vanuatu',
|
||||
'Venezuela',
|
||||
'Vietnam',
|
||||
'Yemen',
|
||||
'Zambia',
|
||||
'Zimbabwe',
|
||||
];
|
||||
|
|
|
|||
|
|
@ -0,0 +1,108 @@
|
|||
@import url('https://fonts.googleapis.com/icon?family=Material+Symbols+Outlined');
|
||||
|
||||
:host {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
font-family: var(--inter-font);
|
||||
--primary: var(--hot-pink);
|
||||
}
|
||||
|
||||
.material-autocomplete {
|
||||
display: flex;
|
||||
position: relative;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.material-symbols-outlined {
|
||||
font-size: 1.25rem;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.search-icon {
|
||||
left: 0.75rem;
|
||||
position: absolute;
|
||||
color: var(--quaternary-contrast);
|
||||
}
|
||||
|
||||
[ngComboboxInput] {
|
||||
width: 13rem;
|
||||
font-size: 1rem;
|
||||
border-radius: 3rem;
|
||||
padding: 0.75rem 0.5rem 0.75rem 2.5rem;
|
||||
color: var(--primary-contrast);
|
||||
outline-color: var(--primary);
|
||||
border: 1px solid var(--quinary-contrast);
|
||||
background-color: var(--page-background);
|
||||
}
|
||||
|
||||
[ngComboboxInput]::placeholder {
|
||||
color: var(--quaternary-contrast);
|
||||
}
|
||||
|
||||
[ngCombobox]:focus-within [ngComboboxInput] {
|
||||
outline: 2px solid var(--primary);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
[ngCombobox]:has([aria-expanded='false']) .popup {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.popup {
|
||||
width: 100%;
|
||||
margin-top: 8px;
|
||||
padding: 0.5rem;
|
||||
max-height: 11rem;
|
||||
border-radius: 2rem;
|
||||
background-color: var(--septenary-contrast);
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.no-results {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
[ngListbox] {
|
||||
gap: 2px;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
overflow: auto;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
[ngOption] {
|
||||
display: flex;
|
||||
cursor: pointer;
|
||||
align-items: center;
|
||||
margin: 1px;
|
||||
padding: 0 1rem;
|
||||
min-height: 3rem;
|
||||
border-radius: 3rem;
|
||||
}
|
||||
|
||||
[ngOption]:hover,
|
||||
[ngOption][data-active='true'] {
|
||||
background-color: color-mix(in srgb, var(--primary-contrast) 5%, transparent);
|
||||
}
|
||||
|
||||
[ngOption][data-active='true'] {
|
||||
outline-offset: -2px;
|
||||
outline: 2px solid var(--primary);
|
||||
}
|
||||
|
||||
[ngOption][aria-selected='true'] {
|
||||
color: var(--primary);
|
||||
background-color: color-mix(in srgb, var(--primary) 10%, transparent);
|
||||
}
|
||||
|
||||
[ngOption]:not([aria-selected='true']) .check-icon {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.option-label {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.check-icon {
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
<div ngCombobox filterMode="highlight">
|
||||
<div #origin class="material-autocomplete">
|
||||
<span class="search-icon material-symbols-outlined">search</span>
|
||||
<input
|
||||
aria-label="Label dropdown"
|
||||
placeholder="Select a country"
|
||||
[(ngModel)]="query"
|
||||
ngComboboxInput
|
||||
/>
|
||||
</div>
|
||||
|
||||
<ng-template ngComboboxPopupContainer>
|
||||
<ng-template
|
||||
[cdkConnectedOverlay]="{origin, usePopover: 'inline', matchWidth: true}"
|
||||
[cdkConnectedOverlayOpen]="true"
|
||||
>
|
||||
<div class="popup">
|
||||
@if (countries().length === 0) {
|
||||
<div class="no-results">No results found</div>
|
||||
}
|
||||
|
||||
<div ngListbox>
|
||||
@for (country of countries(); track country) {
|
||||
<div ngOption [value]="country" [label]="country">
|
||||
<span class="option-label">{{country}}</span>
|
||||
<span class="check-icon material-symbols-outlined">check</span>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</ng-template>
|
||||
</ng-template>
|
||||
</div>
|
||||
|
|
@ -0,0 +1,261 @@
|
|||
import {Combobox, ComboboxInput, ComboboxPopupContainer} from '@angular/aria/combobox';
|
||||
import {Listbox, Option} from '@angular/aria/listbox';
|
||||
import {OverlayModule} from '@angular/cdk/overlay';
|
||||
import {
|
||||
afterRenderEffect,
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
computed,
|
||||
signal,
|
||||
viewChild,
|
||||
viewChildren,
|
||||
} from '@angular/core';
|
||||
import {FormsModule} from '@angular/forms';
|
||||
|
||||
@Component({
|
||||
selector: 'app-root',
|
||||
templateUrl: 'app.component.html',
|
||||
styleUrl: 'app.component.css',
|
||||
imports: [
|
||||
Combobox,
|
||||
ComboboxInput,
|
||||
ComboboxPopupContainer,
|
||||
Listbox,
|
||||
Option,
|
||||
OverlayModule,
|
||||
FormsModule,
|
||||
],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class App {
|
||||
/** The combobox listbox popup. */
|
||||
listbox = viewChild<Listbox<string>>(Listbox);
|
||||
|
||||
/** The options available in the listbox. */
|
||||
options = viewChildren<Option<string>>(Option);
|
||||
|
||||
/** A reference to the ng aria combobox. */
|
||||
combobox = viewChild<Combobox<string>>(Combobox);
|
||||
|
||||
/** The query string used to filter the list of countries. */
|
||||
query = signal('');
|
||||
|
||||
/** The list of countries filtered by the query. */
|
||||
countries = computed(() =>
|
||||
ALL_COUNTRIES.filter((country) => country.toLowerCase().startsWith(this.query().toLowerCase())),
|
||||
);
|
||||
|
||||
constructor() {
|
||||
// Scrolls to the active item when the active option changes.
|
||||
// The slight delay here is to ensure animations are done before scrolling.
|
||||
afterRenderEffect(() => {
|
||||
const option = this.options().find((opt) => opt.active());
|
||||
setTimeout(() => option?.element.scrollIntoView({block: 'nearest'}), 50);
|
||||
});
|
||||
|
||||
// Resets the listbox scroll position when the combobox is closed.
|
||||
afterRenderEffect(() => {
|
||||
if (!this.combobox()?.expanded()) {
|
||||
setTimeout(() => this.listbox()?.element.scrollTo(0, 0), 150);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const ALL_COUNTRIES = [
|
||||
'Afghanistan',
|
||||
'Albania',
|
||||
'Algeria',
|
||||
'Andorra',
|
||||
'Angola',
|
||||
'Antigua and Barbuda',
|
||||
'Argentina',
|
||||
'Armenia',
|
||||
'Australia',
|
||||
'Austria',
|
||||
'Azerbaijan',
|
||||
'Bahamas',
|
||||
'Bahrain',
|
||||
'Bangladesh',
|
||||
'Barbados',
|
||||
'Belarus',
|
||||
'Belgium',
|
||||
'Belize',
|
||||
'Benin',
|
||||
'Bhutan',
|
||||
'Bolivia',
|
||||
'Bosnia and Herzegovina',
|
||||
'Botswana',
|
||||
'Brazil',
|
||||
'Brunei',
|
||||
'Bulgaria',
|
||||
'Burkina Faso',
|
||||
'Burundi',
|
||||
'Cabo Verde',
|
||||
'Cambodia',
|
||||
'Cameroon',
|
||||
'Canada',
|
||||
'Central African Republic',
|
||||
'Chad',
|
||||
'Chile',
|
||||
'China',
|
||||
'Colombia',
|
||||
'Comoros',
|
||||
'Congo (Congo-Brazzaville)',
|
||||
'Costa Rica',
|
||||
"Côte d'Ivoire",
|
||||
'Croatia',
|
||||
'Cuba',
|
||||
'Cyprus',
|
||||
'Czechia (Czech Republic)',
|
||||
'Democratic Republic of the Congo',
|
||||
'Denmark',
|
||||
'Djibouti',
|
||||
'Dominica',
|
||||
'Dominican Republic',
|
||||
'Ecuador',
|
||||
'Egypt',
|
||||
'El Salvador',
|
||||
'Equatorial Guinea',
|
||||
'Eritrea',
|
||||
'Estonia',
|
||||
'Eswatini (fmr. ""Swaziland"")',
|
||||
'Ethiopia',
|
||||
'Fiji',
|
||||
'Finland',
|
||||
'France',
|
||||
'Gabon',
|
||||
'Gambia',
|
||||
'Georgia',
|
||||
'Germany',
|
||||
'Ghana',
|
||||
'Greece',
|
||||
'Grenada',
|
||||
'Guatemala',
|
||||
'Guinea',
|
||||
'Guinea-Bissau',
|
||||
'Guyana',
|
||||
'Haiti',
|
||||
'Holy See',
|
||||
'Honduras',
|
||||
'Hungary',
|
||||
'Iceland',
|
||||
'India',
|
||||
'Indonesia',
|
||||
'Iran',
|
||||
'Iraq',
|
||||
'Ireland',
|
||||
'Israel',
|
||||
'Italy',
|
||||
'Jamaica',
|
||||
'Japan',
|
||||
'Jordan',
|
||||
'Kazakhstan',
|
||||
'Kenya',
|
||||
'Kiribati',
|
||||
'Kuwait',
|
||||
'Kyrgyzstan',
|
||||
'Laos',
|
||||
'Latvia',
|
||||
'Lebanon',
|
||||
'Lesotho',
|
||||
'Liberia',
|
||||
'Libya',
|
||||
'Liechtenstein',
|
||||
'Lithuania',
|
||||
'Luxembourg',
|
||||
'Madagascar',
|
||||
'Malawi',
|
||||
'Malaysia',
|
||||
'Maldives',
|
||||
'Mali',
|
||||
'Malta',
|
||||
'Marshall Islands',
|
||||
'Mauritania',
|
||||
'Mauritius',
|
||||
'Mexico',
|
||||
'Micronesia',
|
||||
'Moldova',
|
||||
'Monaco',
|
||||
'Mongolia',
|
||||
'Montenegro',
|
||||
'Morocco',
|
||||
'Mozambique',
|
||||
'Myanmar (formerly Burma)',
|
||||
'Namibia',
|
||||
'Nauru',
|
||||
'Nepal',
|
||||
'Netherlands',
|
||||
'New Zealand',
|
||||
'Nicaragua',
|
||||
'Niger',
|
||||
'Nigeria',
|
||||
'North Korea',
|
||||
'North Macedonia',
|
||||
'Norway',
|
||||
'Oman',
|
||||
'Pakistan',
|
||||
'Palau',
|
||||
'Palestine State',
|
||||
'Panama',
|
||||
'Papua New Guinea',
|
||||
'Paraguay',
|
||||
'Peru',
|
||||
'Philippines',
|
||||
'Poland',
|
||||
'Portugal',
|
||||
'Qatar',
|
||||
'Romania',
|
||||
'Russia',
|
||||
'Rwanda',
|
||||
'Saint Kitts and Nevis',
|
||||
'Saint Lucia',
|
||||
'Saint Vincent and the Grenadines',
|
||||
'Samoa',
|
||||
'San Marino',
|
||||
'Sao Tome and Principe',
|
||||
'Saudi Arabia',
|
||||
'Senegal',
|
||||
'Serbia',
|
||||
'Seychelles',
|
||||
'Sierra Leone',
|
||||
'Singapore',
|
||||
'Slovakia',
|
||||
'Slovenia',
|
||||
'Solomon Islands',
|
||||
'Somalia',
|
||||
'South Africa',
|
||||
'South Korea',
|
||||
'South Sudan',
|
||||
'Spain',
|
||||
'Sri Lanka',
|
||||
'Sudan',
|
||||
'Suriname',
|
||||
'Sweden',
|
||||
'Switzerland',
|
||||
'Syria',
|
||||
'Tajikistan',
|
||||
'Tanzania',
|
||||
'Thailand',
|
||||
'Timor-Leste',
|
||||
'Togo',
|
||||
'Tonga',
|
||||
'Trinidad and Tobago',
|
||||
'Tunisia',
|
||||
'Turkey',
|
||||
'Turkmenistan',
|
||||
'Tuvalu',
|
||||
'Uganda',
|
||||
'Ukraine',
|
||||
'United Arab Emirates',
|
||||
'United Kingdom',
|
||||
'United States of America',
|
||||
'Uruguay',
|
||||
'Uzbekistan',
|
||||
'Vanuatu',
|
||||
'Venezuela',
|
||||
'Vietnam',
|
||||
'Yemen',
|
||||
'Zambia',
|
||||
'Zimbabwe',
|
||||
];
|
||||
|
|
@ -0,0 +1,130 @@
|
|||
@import url('https://fonts.googleapis.com/icon?family=Material+Symbols+Outlined');
|
||||
@import url('https://fonts.googleapis.com/css2?family=Press+Start+2P&display=swap');
|
||||
|
||||
:host {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
font-size: 0.6rem;
|
||||
font-family: 'Press Start 2P';
|
||||
|
||||
--retro-button-color: #fff;
|
||||
--retro-shadow-light: color-mix(in srgb, var(--retro-button-color) 90%, #fff);
|
||||
--retro-shadow-dark: color-mix(in srgb, var(--retro-button-color) 90%, #000);
|
||||
--retro-elevated-shadow:
|
||||
inset 4px 4px 0px 0px var(--retro-shadow-light),
|
||||
inset -4px -4px 0px 0px var(--retro-shadow-dark), 4px 0px 0px 0px var(--gray-700),
|
||||
0px 4px 0px 0px var(--gray-700), -4px 0px 0px 0px var(--gray-700),
|
||||
0px -4px 0px 0px var(--gray-700);
|
||||
--retro-flat-shadow:
|
||||
4px 0px 0px 0px var(--gray-700), 0px 4px 0px 0px var(--gray-700),
|
||||
-4px 0px 0px 0px var(--gray-700), 0px -4px 0px 0px var(--gray-700);
|
||||
--retro-pressed-shadow:
|
||||
inset 4px 4px 0px 0px var(--retro-shadow-dark),
|
||||
inset -4px -4px 0px 0px var(--retro-shadow-light), 4px 0px 0px 0px var(--gray-700),
|
||||
0px 4px 0px 0px var(--gray-700), -4px 0px 0px 0px var(--gray-700),
|
||||
0px -4px 0px 0px var(--gray-700), 0px 0px 0px 0px var(--gray-700);
|
||||
}
|
||||
|
||||
.retro-autocomplete {
|
||||
display: flex;
|
||||
position: relative;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.material-symbols-outlined {
|
||||
font-size: 1.25rem;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.search-icon {
|
||||
left: 0.75rem;
|
||||
position: absolute;
|
||||
color: #000;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
[ngComboboxInput] {
|
||||
width: 15rem;
|
||||
font-size: 0.6rem;
|
||||
border-radius: 0;
|
||||
font-family: 'Press Start 2P';
|
||||
word-spacing: -5px;
|
||||
padding: 0.75rem 0.5rem 0.75rem 2.5rem;
|
||||
color: #000;
|
||||
border: none;
|
||||
box-shadow: var(--retro-flat-shadow);
|
||||
background-color: var(--retro-button-color);
|
||||
}
|
||||
|
||||
[ngComboboxInput]::placeholder {
|
||||
color: #000;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
[ngComboboxInput]:focus {
|
||||
outline: none;
|
||||
transform: translate(1px, 1px);
|
||||
box-shadow: var(--retro-pressed-shadow);
|
||||
}
|
||||
|
||||
[ngCombobox]:has([aria-expanded='false']) .popup {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.popup {
|
||||
width: 100%;
|
||||
margin-top: 20px;
|
||||
padding: 0.5rem;
|
||||
max-height: 11rem;
|
||||
border-radius: 0;
|
||||
background-color: var(--septenary-contrast);
|
||||
box-shadow: var(--retro-flat-shadow);
|
||||
}
|
||||
|
||||
.no-results {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
[ngListbox] {
|
||||
gap: 2px;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
overflow: auto;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
[ngOption] {
|
||||
display: flex;
|
||||
cursor: pointer;
|
||||
align-items: center;
|
||||
margin: 1px;
|
||||
padding: 0 1rem;
|
||||
min-height: 2.25rem;
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
[ngOption]:hover {
|
||||
background-color: color-mix(in srgb, var(--primary-contrast) 5%, transparent);
|
||||
}
|
||||
|
||||
[ngOption][data-active='true'] {
|
||||
outline-offset: -2px;
|
||||
outline: 2px dashed var(--hot-pink);
|
||||
}
|
||||
|
||||
[ngOption][aria-selected='true'] {
|
||||
color: var(--hot-pink);
|
||||
background-color: color-mix(in srgb, var(--hot-pink) 5%, transparent);
|
||||
}
|
||||
|
||||
[ngOption]:not([aria-selected='true']) .check-icon {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.option-label {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.check-icon {
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
<div ngCombobox filterMode="highlight">
|
||||
<div #origin class="retro-autocomplete">
|
||||
<span class="search-icon material-symbols-outlined">search</span>
|
||||
<input
|
||||
aria-label="Label dropdown"
|
||||
placeholder="Select a country"
|
||||
[(ngModel)]="query"
|
||||
ngComboboxInput
|
||||
/>
|
||||
</div>
|
||||
|
||||
<ng-template ngComboboxPopupContainer>
|
||||
<ng-template
|
||||
[cdkConnectedOverlay]="{origin, usePopover: 'inline', matchWidth: true}"
|
||||
[cdkConnectedOverlayOpen]="true"
|
||||
>
|
||||
<div class="popup">
|
||||
@if (countries().length === 0) {
|
||||
<div class="no-results">No results found</div>
|
||||
}
|
||||
|
||||
<div ngListbox>
|
||||
@for (country of countries(); track country) {
|
||||
<div ngOption [value]="country" [label]="country">
|
||||
<span class="option-label">{{country}}</span>
|
||||
<span class="check-icon material-symbols-outlined">check</span>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</ng-template>
|
||||
</ng-template>
|
||||
</div>
|
||||
|
|
@ -0,0 +1,261 @@
|
|||
import {Combobox, ComboboxInput, ComboboxPopupContainer} from '@angular/aria/combobox';
|
||||
import {Listbox, Option} from '@angular/aria/listbox';
|
||||
import {OverlayModule} from '@angular/cdk/overlay';
|
||||
import {
|
||||
afterRenderEffect,
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
computed,
|
||||
signal,
|
||||
viewChild,
|
||||
viewChildren,
|
||||
} from '@angular/core';
|
||||
import {FormsModule} from '@angular/forms';
|
||||
|
||||
@Component({
|
||||
selector: 'app-root',
|
||||
templateUrl: 'app.component.html',
|
||||
styleUrl: 'app.component.css',
|
||||
imports: [
|
||||
Combobox,
|
||||
ComboboxInput,
|
||||
ComboboxPopupContainer,
|
||||
Listbox,
|
||||
Option,
|
||||
OverlayModule,
|
||||
FormsModule,
|
||||
],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class App {
|
||||
/** The combobox listbox popup. */
|
||||
listbox = viewChild<Listbox<string>>(Listbox);
|
||||
|
||||
/** The options available in the listbox. */
|
||||
options = viewChildren<Option<string>>(Option);
|
||||
|
||||
/** A reference to the ng aria combobox. */
|
||||
combobox = viewChild<Combobox<string>>(Combobox);
|
||||
|
||||
/** The query string used to filter the list of countries. */
|
||||
query = signal('');
|
||||
|
||||
/** The list of countries filtered by the query. */
|
||||
countries = computed(() =>
|
||||
ALL_COUNTRIES.filter((country) => country.toLowerCase().startsWith(this.query().toLowerCase())),
|
||||
);
|
||||
|
||||
constructor() {
|
||||
// Scrolls to the active item when the active option changes.
|
||||
// The slight delay here is to ensure animations are done before scrolling.
|
||||
afterRenderEffect(() => {
|
||||
const option = this.options().find((opt) => opt.active());
|
||||
setTimeout(() => option?.element.scrollIntoView({block: 'nearest'}), 50);
|
||||
});
|
||||
|
||||
// Resets the listbox scroll position when the combobox is closed.
|
||||
afterRenderEffect(() => {
|
||||
if (!this.combobox()?.expanded()) {
|
||||
setTimeout(() => this.listbox()?.element.scrollTo(0, 0), 150);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const ALL_COUNTRIES = [
|
||||
'Afghanistan',
|
||||
'Albania',
|
||||
'Algeria',
|
||||
'Andorra',
|
||||
'Angola',
|
||||
'Antigua and Barbuda',
|
||||
'Argentina',
|
||||
'Armenia',
|
||||
'Australia',
|
||||
'Austria',
|
||||
'Azerbaijan',
|
||||
'Bahamas',
|
||||
'Bahrain',
|
||||
'Bangladesh',
|
||||
'Barbados',
|
||||
'Belarus',
|
||||
'Belgium',
|
||||
'Belize',
|
||||
'Benin',
|
||||
'Bhutan',
|
||||
'Bolivia',
|
||||
'Bosnia and Herzegovina',
|
||||
'Botswana',
|
||||
'Brazil',
|
||||
'Brunei',
|
||||
'Bulgaria',
|
||||
'Burkina Faso',
|
||||
'Burundi',
|
||||
'Cabo Verde',
|
||||
'Cambodia',
|
||||
'Cameroon',
|
||||
'Canada',
|
||||
'Central African Republic',
|
||||
'Chad',
|
||||
'Chile',
|
||||
'China',
|
||||
'Colombia',
|
||||
'Comoros',
|
||||
'Congo (Congo-Brazzaville)',
|
||||
'Costa Rica',
|
||||
"Côte d'Ivoire",
|
||||
'Croatia',
|
||||
'Cuba',
|
||||
'Cyprus',
|
||||
'Czechia (Czech Republic)',
|
||||
'Democratic Republic of the Congo',
|
||||
'Denmark',
|
||||
'Djibouti',
|
||||
'Dominica',
|
||||
'Dominican Republic',
|
||||
'Ecuador',
|
||||
'Egypt',
|
||||
'El Salvador',
|
||||
'Equatorial Guinea',
|
||||
'Eritrea',
|
||||
'Estonia',
|
||||
'Eswatini (fmr. ""Swaziland"")',
|
||||
'Ethiopia',
|
||||
'Fiji',
|
||||
'Finland',
|
||||
'France',
|
||||
'Gabon',
|
||||
'Gambia',
|
||||
'Georgia',
|
||||
'Germany',
|
||||
'Ghana',
|
||||
'Greece',
|
||||
'Grenada',
|
||||
'Guatemala',
|
||||
'Guinea',
|
||||
'Guinea-Bissau',
|
||||
'Guyana',
|
||||
'Haiti',
|
||||
'Holy See',
|
||||
'Honduras',
|
||||
'Hungary',
|
||||
'Iceland',
|
||||
'India',
|
||||
'Indonesia',
|
||||
'Iran',
|
||||
'Iraq',
|
||||
'Ireland',
|
||||
'Israel',
|
||||
'Italy',
|
||||
'Jamaica',
|
||||
'Japan',
|
||||
'Jordan',
|
||||
'Kazakhstan',
|
||||
'Kenya',
|
||||
'Kiribati',
|
||||
'Kuwait',
|
||||
'Kyrgyzstan',
|
||||
'Laos',
|
||||
'Latvia',
|
||||
'Lebanon',
|
||||
'Lesotho',
|
||||
'Liberia',
|
||||
'Libya',
|
||||
'Liechtenstein',
|
||||
'Lithuania',
|
||||
'Luxembourg',
|
||||
'Madagascar',
|
||||
'Malawi',
|
||||
'Malaysia',
|
||||
'Maldives',
|
||||
'Mali',
|
||||
'Malta',
|
||||
'Marshall Islands',
|
||||
'Mauritania',
|
||||
'Mauritius',
|
||||
'Mexico',
|
||||
'Micronesia',
|
||||
'Moldova',
|
||||
'Monaco',
|
||||
'Mongolia',
|
||||
'Montenegro',
|
||||
'Morocco',
|
||||
'Mozambique',
|
||||
'Myanmar (formerly Burma)',
|
||||
'Namibia',
|
||||
'Nauru',
|
||||
'Nepal',
|
||||
'Netherlands',
|
||||
'New Zealand',
|
||||
'Nicaragua',
|
||||
'Niger',
|
||||
'Nigeria',
|
||||
'North Korea',
|
||||
'North Macedonia',
|
||||
'Norway',
|
||||
'Oman',
|
||||
'Pakistan',
|
||||
'Palau',
|
||||
'Palestine State',
|
||||
'Panama',
|
||||
'Papua New Guinea',
|
||||
'Paraguay',
|
||||
'Peru',
|
||||
'Philippines',
|
||||
'Poland',
|
||||
'Portugal',
|
||||
'Qatar',
|
||||
'Romania',
|
||||
'Russia',
|
||||
'Rwanda',
|
||||
'Saint Kitts and Nevis',
|
||||
'Saint Lucia',
|
||||
'Saint Vincent and the Grenadines',
|
||||
'Samoa',
|
||||
'San Marino',
|
||||
'Sao Tome and Principe',
|
||||
'Saudi Arabia',
|
||||
'Senegal',
|
||||
'Serbia',
|
||||
'Seychelles',
|
||||
'Sierra Leone',
|
||||
'Singapore',
|
||||
'Slovakia',
|
||||
'Slovenia',
|
||||
'Solomon Islands',
|
||||
'Somalia',
|
||||
'South Africa',
|
||||
'South Korea',
|
||||
'South Sudan',
|
||||
'Spain',
|
||||
'Sri Lanka',
|
||||
'Sudan',
|
||||
'Suriname',
|
||||
'Sweden',
|
||||
'Switzerland',
|
||||
'Syria',
|
||||
'Tajikistan',
|
||||
'Tanzania',
|
||||
'Thailand',
|
||||
'Timor-Leste',
|
||||
'Togo',
|
||||
'Tonga',
|
||||
'Trinidad and Tobago',
|
||||
'Tunisia',
|
||||
'Turkey',
|
||||
'Turkmenistan',
|
||||
'Tuvalu',
|
||||
'Uganda',
|
||||
'Ukraine',
|
||||
'United Arab Emirates',
|
||||
'United Kingdom',
|
||||
'United States of America',
|
||||
'Uruguay',
|
||||
'Uzbekistan',
|
||||
'Vanuatu',
|
||||
'Venezuela',
|
||||
'Vietnam',
|
||||
'Yemen',
|
||||
'Zambia',
|
||||
'Zimbabwe',
|
||||
];
|
||||
|
|
@ -1,89 +1,101 @@
|
|||
html {
|
||||
@import url('https://fonts.googleapis.com/icon?family=Material+Symbols+Outlined');
|
||||
|
||||
:host {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
font-family: var(--inter-font);
|
||||
}
|
||||
|
||||
.combobox-container {
|
||||
max-width: 400px;
|
||||
margin: 20px;
|
||||
}
|
||||
|
||||
label {
|
||||
display: block;
|
||||
margin-bottom: 8px;
|
||||
font-weight: 500;
|
||||
color: #e0e0e0;
|
||||
}
|
||||
|
||||
.input-container {
|
||||
.autocomplete {
|
||||
display: flex;
|
||||
position: relative;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.combobox-input {
|
||||
.material-symbols-outlined {
|
||||
font-size: 1.25rem;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.search-icon {
|
||||
left: 0.75rem;
|
||||
position: absolute;
|
||||
color: var(--quaternary-contrast);
|
||||
}
|
||||
|
||||
[ngComboboxInput] {
|
||||
width: 13rem;
|
||||
font-size: 1rem;
|
||||
border-radius: 0.25rem;
|
||||
padding: 0.75rem 0.5rem 0.75rem 2.5rem;
|
||||
color: var(--primary-contrast);
|
||||
outline-color: var(--hot-pink);
|
||||
border: 1px solid var(--quinary-contrast);
|
||||
background-color: var(--page-background);
|
||||
}
|
||||
|
||||
[ngComboboxInput]::placeholder {
|
||||
color: var(--quaternary-contrast);
|
||||
}
|
||||
|
||||
[ngCombobox]:has([aria-expanded='false']) .popup {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.popup {
|
||||
width: 100%;
|
||||
padding: 10px 12px;
|
||||
border: 1px solid #404040;
|
||||
border-radius: 4px;
|
||||
font-size: 16px;
|
||||
box-sizing: border-box;
|
||||
background-color: #1a1a1a;
|
||||
color: #e0e0e0;
|
||||
margin-top: 8px;
|
||||
padding: 0.5rem;
|
||||
max-height: 11rem;
|
||||
border-radius: 0.5rem;
|
||||
background-color: var(--septenary-contrast);
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.combobox-input::placeholder {
|
||||
color: #888;
|
||||
.no-results {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.combobox-input:focus {
|
||||
outline: none;
|
||||
border-color: #4a9eff;
|
||||
background-color: #1f1f1f;
|
||||
[ngListbox] {
|
||||
gap: 2px;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
overflow: auto;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.popover {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
border: 1px solid #404040;
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.5);
|
||||
background: #1a1a1a;
|
||||
max-height: 300px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.listbox {
|
||||
padding: 4px 0;
|
||||
}
|
||||
|
||||
.option {
|
||||
padding: 10px 12px;
|
||||
[ngOption] {
|
||||
display: flex;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
color: #e0e0e0;
|
||||
align-items: center;
|
||||
margin: 1px;
|
||||
padding: 0 1rem;
|
||||
min-height: 2.25rem;
|
||||
border-radius: 0.5rem;
|
||||
}
|
||||
|
||||
.option:hover {
|
||||
background-color: #2a2a2a;
|
||||
[ngOption]:hover {
|
||||
background-color: color-mix(in srgb, var(--primary-contrast) 5%, transparent);
|
||||
}
|
||||
|
||||
.option[data-active] {
|
||||
background-color: #2d4a6e;
|
||||
color: #ffffff;
|
||||
[ngOption][data-active='true'] {
|
||||
outline-offset: -2px;
|
||||
outline: 2px solid var(--hot-pink);
|
||||
}
|
||||
|
||||
.option[aria-selected='true'] {
|
||||
background-color: #4a9eff;
|
||||
color: #000000;
|
||||
[ngOption][aria-selected='true'] {
|
||||
color: var(--hot-pink);
|
||||
background-color: color-mix(in srgb, var(--hot-pink) 5%, transparent);
|
||||
}
|
||||
|
||||
.info {
|
||||
margin: 20px;
|
||||
padding: 16px;
|
||||
background-color: #1f1f1f;
|
||||
border-radius: 4px;
|
||||
border-left: 4px solid #4a9eff;
|
||||
color: #e0e0e0;
|
||||
[ngOption]:not([aria-selected='true']) .check-icon {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.info p {
|
||||
margin: 8px 0;
|
||||
.option-label {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.check-icon {
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,31 +1,33 @@
|
|||
<div ngCombobox #combobox="ngCombobox" filterMode="manual" class="combobox-container">
|
||||
<label for="state-input">Select a state:</label>
|
||||
<div class="input-container">
|
||||
<div ngCombobox focusMode="manual">
|
||||
<div #origin class="autocomplete">
|
||||
<span class="search-icon material-symbols-outlined">search</span>
|
||||
<input
|
||||
id="state-input"
|
||||
aria-label="Label dropdown"
|
||||
placeholder="Select a country"
|
||||
[(ngModel)]="query"
|
||||
ngComboboxInput
|
||||
placeholder="Search states..."
|
||||
[(value)]="searchString"
|
||||
class="combobox-input"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div popover="manual" #popover class="popover">
|
||||
<ng-template ngComboboxPopupContainer>
|
||||
<div ngListbox class="listbox">
|
||||
@for (option of options(); track option) {
|
||||
<div class="option" ngOption [value]="option" [label]="option">
|
||||
<span>{{ option }}</span>
|
||||
</div>
|
||||
<ng-template ngComboboxPopupContainer>
|
||||
<ng-template
|
||||
[cdkConnectedOverlay]="{origin, usePopover: 'inline', matchWidth: true}"
|
||||
[cdkConnectedOverlayOpen]="true"
|
||||
>
|
||||
<div class="popup">
|
||||
@if (countries().length === 0) {
|
||||
<div class="no-results">No results found</div>
|
||||
}
|
||||
|
||||
<div ngListbox>
|
||||
@for (country of countries(); track country) {
|
||||
<div ngOption [value]="country" [label]="country">
|
||||
<span class="option-label">{{country}}</span>
|
||||
<span class="check-icon material-symbols-outlined">check</span>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</ng-template>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="info">
|
||||
<p><strong>Manual mode:</strong> The input only updates when you explicitly select an option with Enter or click.</p>
|
||||
@if (searchString()) {
|
||||
<p>Current value: <strong>{{ searchString() }}</strong></p>
|
||||
}
|
||||
</ng-template>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,108 +1,261 @@
|
|||
import {Combobox, ComboboxInput, ComboboxPopupContainer} from '@angular/aria/combobox';
|
||||
import {Listbox, Option} from '@angular/aria/listbox';
|
||||
import {OverlayModule} from '@angular/cdk/overlay';
|
||||
import {
|
||||
afterRenderEffect,
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
computed,
|
||||
ElementRef,
|
||||
signal,
|
||||
viewChild,
|
||||
viewChildren,
|
||||
} from '@angular/core';
|
||||
import {FormsModule} from '@angular/forms';
|
||||
|
||||
@Component({
|
||||
selector: 'app-root',
|
||||
templateUrl: 'app.component.html',
|
||||
styleUrl: 'app.component.css',
|
||||
imports: [Combobox, ComboboxInput, ComboboxPopupContainer, Listbox, Option],
|
||||
imports: [
|
||||
Combobox,
|
||||
ComboboxInput,
|
||||
ComboboxPopupContainer,
|
||||
Listbox,
|
||||
Option,
|
||||
OverlayModule,
|
||||
FormsModule,
|
||||
],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class App {
|
||||
popover = viewChild<ElementRef>('popover');
|
||||
listbox = viewChild<Listbox<any>>(Listbox);
|
||||
combobox = viewChild<Combobox<any>>(Combobox);
|
||||
/** The combobox listbox popup. */
|
||||
listbox = viewChild<Listbox<string>>(Listbox);
|
||||
|
||||
searchString = signal('');
|
||||
/** The options available in the listbox. */
|
||||
options = viewChildren<Option<string>>(Option);
|
||||
|
||||
options = computed(() =>
|
||||
states.filter((state) => state.toLowerCase().startsWith(this.searchString().toLowerCase())),
|
||||
/** A reference to the ng aria combobox. */
|
||||
combobox = viewChild<Combobox<string>>(Combobox);
|
||||
|
||||
/** The query string used to filter the list of countries. */
|
||||
query = signal('');
|
||||
|
||||
/** The list of countries filtered by the query. */
|
||||
countries = computed(() =>
|
||||
ALL_COUNTRIES.filter((country) => country.toLowerCase().startsWith(this.query().toLowerCase())),
|
||||
);
|
||||
|
||||
constructor() {
|
||||
// Scrolls to the active item when the active option changes.
|
||||
// The slight delay here is to ensure animations are done before scrolling.
|
||||
afterRenderEffect(() => {
|
||||
const popover = this.popover()!;
|
||||
const combobox = this.combobox()!;
|
||||
combobox.expanded() ? this.showPopover() : popover.nativeElement.hidePopover();
|
||||
this.listbox()?.scrollActiveItemIntoView();
|
||||
const option = this.options().find((opt) => opt.active());
|
||||
setTimeout(() => option?.element.scrollIntoView({block: 'nearest'}), 50);
|
||||
});
|
||||
}
|
||||
|
||||
showPopover() {
|
||||
const popover = this.popover()!;
|
||||
const combobox = this.combobox()!;
|
||||
|
||||
const comboboxRect = combobox.inputElement()?.getBoundingClientRect();
|
||||
const popoverEl = popover.nativeElement;
|
||||
|
||||
if (comboboxRect) {
|
||||
popoverEl.style.width = `${comboboxRect.width}px`;
|
||||
popoverEl.style.top = `${comboboxRect.bottom + 4}px`;
|
||||
popoverEl.style.left = `${comboboxRect.left - 1}px`;
|
||||
}
|
||||
|
||||
popover.nativeElement.showPopover();
|
||||
// Resets the listbox scroll position when the combobox is closed.
|
||||
afterRenderEffect(() => {
|
||||
if (!this.combobox()?.expanded()) {
|
||||
setTimeout(() => this.listbox()?.element.scrollTo(0, 0), 150);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const states = [
|
||||
'Alabama',
|
||||
'Alaska',
|
||||
'Arizona',
|
||||
'Arkansas',
|
||||
'California',
|
||||
'Colorado',
|
||||
'Connecticut',
|
||||
'Delaware',
|
||||
'Florida',
|
||||
const ALL_COUNTRIES = [
|
||||
'Afghanistan',
|
||||
'Albania',
|
||||
'Algeria',
|
||||
'Andorra',
|
||||
'Angola',
|
||||
'Antigua and Barbuda',
|
||||
'Argentina',
|
||||
'Armenia',
|
||||
'Australia',
|
||||
'Austria',
|
||||
'Azerbaijan',
|
||||
'Bahamas',
|
||||
'Bahrain',
|
||||
'Bangladesh',
|
||||
'Barbados',
|
||||
'Belarus',
|
||||
'Belgium',
|
||||
'Belize',
|
||||
'Benin',
|
||||
'Bhutan',
|
||||
'Bolivia',
|
||||
'Bosnia and Herzegovina',
|
||||
'Botswana',
|
||||
'Brazil',
|
||||
'Brunei',
|
||||
'Bulgaria',
|
||||
'Burkina Faso',
|
||||
'Burundi',
|
||||
'Cabo Verde',
|
||||
'Cambodia',
|
||||
'Cameroon',
|
||||
'Canada',
|
||||
'Central African Republic',
|
||||
'Chad',
|
||||
'Chile',
|
||||
'China',
|
||||
'Colombia',
|
||||
'Comoros',
|
||||
'Congo (Congo-Brazzaville)',
|
||||
'Costa Rica',
|
||||
"Côte d'Ivoire",
|
||||
'Croatia',
|
||||
'Cuba',
|
||||
'Cyprus',
|
||||
'Czechia (Czech Republic)',
|
||||
'Democratic Republic of the Congo',
|
||||
'Denmark',
|
||||
'Djibouti',
|
||||
'Dominica',
|
||||
'Dominican Republic',
|
||||
'Ecuador',
|
||||
'Egypt',
|
||||
'El Salvador',
|
||||
'Equatorial Guinea',
|
||||
'Eritrea',
|
||||
'Estonia',
|
||||
'Eswatini (fmr. ""Swaziland"")',
|
||||
'Ethiopia',
|
||||
'Fiji',
|
||||
'Finland',
|
||||
'France',
|
||||
'Gabon',
|
||||
'Gambia',
|
||||
'Georgia',
|
||||
'Hawaii',
|
||||
'Idaho',
|
||||
'Illinois',
|
||||
'Indiana',
|
||||
'Iowa',
|
||||
'Kansas',
|
||||
'Kentucky',
|
||||
'Louisiana',
|
||||
'Maine',
|
||||
'Maryland',
|
||||
'Massachusetts',
|
||||
'Michigan',
|
||||
'Minnesota',
|
||||
'Mississippi',
|
||||
'Missouri',
|
||||
'Montana',
|
||||
'Nebraska',
|
||||
'Nevada',
|
||||
'New Hampshire',
|
||||
'New Jersey',
|
||||
'New Mexico',
|
||||
'New York',
|
||||
'North Carolina',
|
||||
'North Dakota',
|
||||
'Ohio',
|
||||
'Oklahoma',
|
||||
'Oregon',
|
||||
'Pennsylvania',
|
||||
'Rhode Island',
|
||||
'South Carolina',
|
||||
'South Dakota',
|
||||
'Tennessee',
|
||||
'Texas',
|
||||
'Utah',
|
||||
'Vermont',
|
||||
'Virginia',
|
||||
'Washington',
|
||||
'West Virginia',
|
||||
'Wisconsin',
|
||||
'Wyoming',
|
||||
'Germany',
|
||||
'Ghana',
|
||||
'Greece',
|
||||
'Grenada',
|
||||
'Guatemala',
|
||||
'Guinea',
|
||||
'Guinea-Bissau',
|
||||
'Guyana',
|
||||
'Haiti',
|
||||
'Holy See',
|
||||
'Honduras',
|
||||
'Hungary',
|
||||
'Iceland',
|
||||
'India',
|
||||
'Indonesia',
|
||||
'Iran',
|
||||
'Iraq',
|
||||
'Ireland',
|
||||
'Israel',
|
||||
'Italy',
|
||||
'Jamaica',
|
||||
'Japan',
|
||||
'Jordan',
|
||||
'Kazakhstan',
|
||||
'Kenya',
|
||||
'Kiribati',
|
||||
'Kuwait',
|
||||
'Kyrgyzstan',
|
||||
'Laos',
|
||||
'Latvia',
|
||||
'Lebanon',
|
||||
'Lesotho',
|
||||
'Liberia',
|
||||
'Libya',
|
||||
'Liechtenstein',
|
||||
'Lithuania',
|
||||
'Luxembourg',
|
||||
'Madagascar',
|
||||
'Malawi',
|
||||
'Malaysia',
|
||||
'Maldives',
|
||||
'Mali',
|
||||
'Malta',
|
||||
'Marshall Islands',
|
||||
'Mauritania',
|
||||
'Mauritius',
|
||||
'Mexico',
|
||||
'Micronesia',
|
||||
'Moldova',
|
||||
'Monaco',
|
||||
'Mongolia',
|
||||
'Montenegro',
|
||||
'Morocco',
|
||||
'Mozambique',
|
||||
'Myanmar (formerly Burma)',
|
||||
'Namibia',
|
||||
'Nauru',
|
||||
'Nepal',
|
||||
'Netherlands',
|
||||
'New Zealand',
|
||||
'Nicaragua',
|
||||
'Niger',
|
||||
'Nigeria',
|
||||
'North Korea',
|
||||
'North Macedonia',
|
||||
'Norway',
|
||||
'Oman',
|
||||
'Pakistan',
|
||||
'Palau',
|
||||
'Palestine State',
|
||||
'Panama',
|
||||
'Papua New Guinea',
|
||||
'Paraguay',
|
||||
'Peru',
|
||||
'Philippines',
|
||||
'Poland',
|
||||
'Portugal',
|
||||
'Qatar',
|
||||
'Romania',
|
||||
'Russia',
|
||||
'Rwanda',
|
||||
'Saint Kitts and Nevis',
|
||||
'Saint Lucia',
|
||||
'Saint Vincent and the Grenadines',
|
||||
'Samoa',
|
||||
'San Marino',
|
||||
'Sao Tome and Principe',
|
||||
'Saudi Arabia',
|
||||
'Senegal',
|
||||
'Serbia',
|
||||
'Seychelles',
|
||||
'Sierra Leone',
|
||||
'Singapore',
|
||||
'Slovakia',
|
||||
'Slovenia',
|
||||
'Solomon Islands',
|
||||
'Somalia',
|
||||
'South Africa',
|
||||
'South Korea',
|
||||
'South Sudan',
|
||||
'Spain',
|
||||
'Sri Lanka',
|
||||
'Sudan',
|
||||
'Suriname',
|
||||
'Sweden',
|
||||
'Switzerland',
|
||||
'Syria',
|
||||
'Tajikistan',
|
||||
'Tanzania',
|
||||
'Thailand',
|
||||
'Timor-Leste',
|
||||
'Togo',
|
||||
'Tonga',
|
||||
'Trinidad and Tobago',
|
||||
'Tunisia',
|
||||
'Turkey',
|
||||
'Turkmenistan',
|
||||
'Tuvalu',
|
||||
'Uganda',
|
||||
'Ukraine',
|
||||
'United Arab Emirates',
|
||||
'United Kingdom',
|
||||
'United States of America',
|
||||
'Uruguay',
|
||||
'Uzbekistan',
|
||||
'Vanuatu',
|
||||
'Venezuela',
|
||||
'Vietnam',
|
||||
'Yemen',
|
||||
'Zambia',
|
||||
'Zimbabwe',
|
||||
];
|
||||
|
|
|
|||
|
|
@ -0,0 +1,108 @@
|
|||
@import url('https://fonts.googleapis.com/icon?family=Material+Symbols+Outlined');
|
||||
|
||||
:host {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
font-family: var(--inter-font);
|
||||
--primary: var(--hot-pink);
|
||||
}
|
||||
|
||||
.material-autocomplete {
|
||||
display: flex;
|
||||
position: relative;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.material-symbols-outlined {
|
||||
font-size: 1.25rem;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.search-icon {
|
||||
left: 0.75rem;
|
||||
position: absolute;
|
||||
color: var(--quaternary-contrast);
|
||||
}
|
||||
|
||||
[ngComboboxInput] {
|
||||
width: 13rem;
|
||||
font-size: 1rem;
|
||||
border-radius: 3rem;
|
||||
padding: 0.75rem 0.5rem 0.75rem 2.5rem;
|
||||
color: var(--primary-contrast);
|
||||
outline-color: var(--primary);
|
||||
border: 1px solid var(--quinary-contrast);
|
||||
background-color: var(--page-background);
|
||||
}
|
||||
|
||||
[ngComboboxInput]::placeholder {
|
||||
color: var(--quaternary-contrast);
|
||||
}
|
||||
|
||||
[ngCombobox]:focus-within [ngComboboxInput] {
|
||||
outline: 2px solid var(--primary);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
[ngCombobox]:has([aria-expanded='false']) .popup {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.popup {
|
||||
width: 100%;
|
||||
margin-top: 8px;
|
||||
padding: 0.5rem;
|
||||
max-height: 11rem;
|
||||
border-radius: 2rem;
|
||||
background-color: var(--septenary-contrast);
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.no-results {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
[ngListbox] {
|
||||
gap: 2px;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
overflow: auto;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
[ngOption] {
|
||||
display: flex;
|
||||
cursor: pointer;
|
||||
align-items: center;
|
||||
margin: 1px;
|
||||
padding: 0 1rem;
|
||||
min-height: 3rem;
|
||||
border-radius: 3rem;
|
||||
}
|
||||
|
||||
[ngOption]:hover,
|
||||
[ngOption][data-active='true'] {
|
||||
background-color: color-mix(in srgb, var(--primary-contrast) 5%, transparent);
|
||||
}
|
||||
|
||||
[ngOption][data-active='true'] {
|
||||
outline-offset: -2px;
|
||||
outline: 2px solid var(--primary);
|
||||
}
|
||||
|
||||
[ngOption][aria-selected='true'] {
|
||||
color: var(--primary);
|
||||
background-color: color-mix(in srgb, var(--primary) 10%, transparent);
|
||||
}
|
||||
|
||||
[ngOption]:not([aria-selected='true']) .check-icon {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.option-label {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.check-icon {
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
<div ngCombobox focusMode="manual">
|
||||
<div #origin class="material-autocomplete">
|
||||
<span class="search-icon material-symbols-outlined">search</span>
|
||||
<input
|
||||
aria-label="Label dropdown"
|
||||
placeholder="Select a country"
|
||||
[(ngModel)]="query"
|
||||
ngComboboxInput
|
||||
/>
|
||||
</div>
|
||||
|
||||
<ng-template ngComboboxPopupContainer>
|
||||
<ng-template
|
||||
[cdkConnectedOverlay]="{origin, usePopover: 'inline', matchWidth: true}"
|
||||
[cdkConnectedOverlayOpen]="true"
|
||||
>
|
||||
<div class="popup">
|
||||
@if (countries().length === 0) {
|
||||
<div class="no-results">No results found</div>
|
||||
}
|
||||
|
||||
<div ngListbox>
|
||||
@for (country of countries(); track country) {
|
||||
<div ngOption [value]="country" [label]="country">
|
||||
<span class="option-label">{{country}}</span>
|
||||
<span class="check-icon material-symbols-outlined">check</span>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</ng-template>
|
||||
</ng-template>
|
||||
</div>
|
||||
|
|
@ -0,0 +1,261 @@
|
|||
import {Combobox, ComboboxInput, ComboboxPopupContainer} from '@angular/aria/combobox';
|
||||
import {Listbox, Option} from '@angular/aria/listbox';
|
||||
import {OverlayModule} from '@angular/cdk/overlay';
|
||||
import {
|
||||
afterRenderEffect,
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
computed,
|
||||
signal,
|
||||
viewChild,
|
||||
viewChildren,
|
||||
} from '@angular/core';
|
||||
import {FormsModule} from '@angular/forms';
|
||||
|
||||
@Component({
|
||||
selector: 'app-root',
|
||||
templateUrl: 'app.component.html',
|
||||
styleUrl: 'app.component.css',
|
||||
imports: [
|
||||
Combobox,
|
||||
ComboboxInput,
|
||||
ComboboxPopupContainer,
|
||||
Listbox,
|
||||
Option,
|
||||
OverlayModule,
|
||||
FormsModule,
|
||||
],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class App {
|
||||
/** The combobox listbox popup. */
|
||||
listbox = viewChild<Listbox<string>>(Listbox);
|
||||
|
||||
/** The options available in the listbox. */
|
||||
options = viewChildren<Option<string>>(Option);
|
||||
|
||||
/** A reference to the ng aria combobox. */
|
||||
combobox = viewChild<Combobox<string>>(Combobox);
|
||||
|
||||
/** The query string used to filter the list of countries. */
|
||||
query = signal('');
|
||||
|
||||
/** The list of countries filtered by the query. */
|
||||
countries = computed(() =>
|
||||
ALL_COUNTRIES.filter((country) => country.toLowerCase().startsWith(this.query().toLowerCase())),
|
||||
);
|
||||
|
||||
constructor() {
|
||||
// Scrolls to the active item when the active option changes.
|
||||
// The slight delay here is to ensure animations are done before scrolling.
|
||||
afterRenderEffect(() => {
|
||||
const option = this.options().find((opt) => opt.active());
|
||||
setTimeout(() => option?.element.scrollIntoView({block: 'nearest'}), 50);
|
||||
});
|
||||
|
||||
// Resets the listbox scroll position when the combobox is closed.
|
||||
afterRenderEffect(() => {
|
||||
if (!this.combobox()?.expanded()) {
|
||||
setTimeout(() => this.listbox()?.element.scrollTo(0, 0), 150);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const ALL_COUNTRIES = [
|
||||
'Afghanistan',
|
||||
'Albania',
|
||||
'Algeria',
|
||||
'Andorra',
|
||||
'Angola',
|
||||
'Antigua and Barbuda',
|
||||
'Argentina',
|
||||
'Armenia',
|
||||
'Australia',
|
||||
'Austria',
|
||||
'Azerbaijan',
|
||||
'Bahamas',
|
||||
'Bahrain',
|
||||
'Bangladesh',
|
||||
'Barbados',
|
||||
'Belarus',
|
||||
'Belgium',
|
||||
'Belize',
|
||||
'Benin',
|
||||
'Bhutan',
|
||||
'Bolivia',
|
||||
'Bosnia and Herzegovina',
|
||||
'Botswana',
|
||||
'Brazil',
|
||||
'Brunei',
|
||||
'Bulgaria',
|
||||
'Burkina Faso',
|
||||
'Burundi',
|
||||
'Cabo Verde',
|
||||
'Cambodia',
|
||||
'Cameroon',
|
||||
'Canada',
|
||||
'Central African Republic',
|
||||
'Chad',
|
||||
'Chile',
|
||||
'China',
|
||||
'Colombia',
|
||||
'Comoros',
|
||||
'Congo (Congo-Brazzaville)',
|
||||
'Costa Rica',
|
||||
"Côte d'Ivoire",
|
||||
'Croatia',
|
||||
'Cuba',
|
||||
'Cyprus',
|
||||
'Czechia (Czech Republic)',
|
||||
'Democratic Republic of the Congo',
|
||||
'Denmark',
|
||||
'Djibouti',
|
||||
'Dominica',
|
||||
'Dominican Republic',
|
||||
'Ecuador',
|
||||
'Egypt',
|
||||
'El Salvador',
|
||||
'Equatorial Guinea',
|
||||
'Eritrea',
|
||||
'Estonia',
|
||||
'Eswatini (fmr. ""Swaziland"")',
|
||||
'Ethiopia',
|
||||
'Fiji',
|
||||
'Finland',
|
||||
'France',
|
||||
'Gabon',
|
||||
'Gambia',
|
||||
'Georgia',
|
||||
'Germany',
|
||||
'Ghana',
|
||||
'Greece',
|
||||
'Grenada',
|
||||
'Guatemala',
|
||||
'Guinea',
|
||||
'Guinea-Bissau',
|
||||
'Guyana',
|
||||
'Haiti',
|
||||
'Holy See',
|
||||
'Honduras',
|
||||
'Hungary',
|
||||
'Iceland',
|
||||
'India',
|
||||
'Indonesia',
|
||||
'Iran',
|
||||
'Iraq',
|
||||
'Ireland',
|
||||
'Israel',
|
||||
'Italy',
|
||||
'Jamaica',
|
||||
'Japan',
|
||||
'Jordan',
|
||||
'Kazakhstan',
|
||||
'Kenya',
|
||||
'Kiribati',
|
||||
'Kuwait',
|
||||
'Kyrgyzstan',
|
||||
'Laos',
|
||||
'Latvia',
|
||||
'Lebanon',
|
||||
'Lesotho',
|
||||
'Liberia',
|
||||
'Libya',
|
||||
'Liechtenstein',
|
||||
'Lithuania',
|
||||
'Luxembourg',
|
||||
'Madagascar',
|
||||
'Malawi',
|
||||
'Malaysia',
|
||||
'Maldives',
|
||||
'Mali',
|
||||
'Malta',
|
||||
'Marshall Islands',
|
||||
'Mauritania',
|
||||
'Mauritius',
|
||||
'Mexico',
|
||||
'Micronesia',
|
||||
'Moldova',
|
||||
'Monaco',
|
||||
'Mongolia',
|
||||
'Montenegro',
|
||||
'Morocco',
|
||||
'Mozambique',
|
||||
'Myanmar (formerly Burma)',
|
||||
'Namibia',
|
||||
'Nauru',
|
||||
'Nepal',
|
||||
'Netherlands',
|
||||
'New Zealand',
|
||||
'Nicaragua',
|
||||
'Niger',
|
||||
'Nigeria',
|
||||
'North Korea',
|
||||
'North Macedonia',
|
||||
'Norway',
|
||||
'Oman',
|
||||
'Pakistan',
|
||||
'Palau',
|
||||
'Palestine State',
|
||||
'Panama',
|
||||
'Papua New Guinea',
|
||||
'Paraguay',
|
||||
'Peru',
|
||||
'Philippines',
|
||||
'Poland',
|
||||
'Portugal',
|
||||
'Qatar',
|
||||
'Romania',
|
||||
'Russia',
|
||||
'Rwanda',
|
||||
'Saint Kitts and Nevis',
|
||||
'Saint Lucia',
|
||||
'Saint Vincent and the Grenadines',
|
||||
'Samoa',
|
||||
'San Marino',
|
||||
'Sao Tome and Principe',
|
||||
'Saudi Arabia',
|
||||
'Senegal',
|
||||
'Serbia',
|
||||
'Seychelles',
|
||||
'Sierra Leone',
|
||||
'Singapore',
|
||||
'Slovakia',
|
||||
'Slovenia',
|
||||
'Solomon Islands',
|
||||
'Somalia',
|
||||
'South Africa',
|
||||
'South Korea',
|
||||
'South Sudan',
|
||||
'Spain',
|
||||
'Sri Lanka',
|
||||
'Sudan',
|
||||
'Suriname',
|
||||
'Sweden',
|
||||
'Switzerland',
|
||||
'Syria',
|
||||
'Tajikistan',
|
||||
'Tanzania',
|
||||
'Thailand',
|
||||
'Timor-Leste',
|
||||
'Togo',
|
||||
'Tonga',
|
||||
'Trinidad and Tobago',
|
||||
'Tunisia',
|
||||
'Turkey',
|
||||
'Turkmenistan',
|
||||
'Tuvalu',
|
||||
'Uganda',
|
||||
'Ukraine',
|
||||
'United Arab Emirates',
|
||||
'United Kingdom',
|
||||
'United States of America',
|
||||
'Uruguay',
|
||||
'Uzbekistan',
|
||||
'Vanuatu',
|
||||
'Venezuela',
|
||||
'Vietnam',
|
||||
'Yemen',
|
||||
'Zambia',
|
||||
'Zimbabwe',
|
||||
];
|
||||
|
|
@ -0,0 +1,130 @@
|
|||
@import url('https://fonts.googleapis.com/icon?family=Material+Symbols+Outlined');
|
||||
@import url('https://fonts.googleapis.com/css2?family=Press+Start+2P&display=swap');
|
||||
|
||||
:host {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
font-size: 0.6rem;
|
||||
font-family: 'Press Start 2P';
|
||||
|
||||
--retro-button-color: #fff;
|
||||
--retro-shadow-light: color-mix(in srgb, var(--retro-button-color) 90%, #fff);
|
||||
--retro-shadow-dark: color-mix(in srgb, var(--retro-button-color) 90%, #000);
|
||||
--retro-elevated-shadow:
|
||||
inset 4px 4px 0px 0px var(--retro-shadow-light),
|
||||
inset -4px -4px 0px 0px var(--retro-shadow-dark), 4px 0px 0px 0px var(--gray-700),
|
||||
0px 4px 0px 0px var(--gray-700), -4px 0px 0px 0px var(--gray-700),
|
||||
0px -4px 0px 0px var(--gray-700);
|
||||
--retro-flat-shadow:
|
||||
4px 0px 0px 0px var(--gray-700), 0px 4px 0px 0px var(--gray-700),
|
||||
-4px 0px 0px 0px var(--gray-700), 0px -4px 0px 0px var(--gray-700);
|
||||
--retro-pressed-shadow:
|
||||
inset 4px 4px 0px 0px var(--retro-shadow-dark),
|
||||
inset -4px -4px 0px 0px var(--retro-shadow-light), 4px 0px 0px 0px var(--gray-700),
|
||||
0px 4px 0px 0px var(--gray-700), -4px 0px 0px 0px var(--gray-700),
|
||||
0px -4px 0px 0px var(--gray-700), 0px 0px 0px 0px var(--gray-700);
|
||||
}
|
||||
|
||||
.retro-autocomplete {
|
||||
display: flex;
|
||||
position: relative;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.material-symbols-outlined {
|
||||
font-size: 1.25rem;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.search-icon {
|
||||
left: 0.75rem;
|
||||
position: absolute;
|
||||
color: #000;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
[ngComboboxInput] {
|
||||
width: 15rem;
|
||||
font-size: 0.6rem;
|
||||
border-radius: 0;
|
||||
font-family: 'Press Start 2P';
|
||||
word-spacing: -5px;
|
||||
padding: 0.75rem 0.5rem 0.75rem 2.5rem;
|
||||
color: #000;
|
||||
border: none;
|
||||
box-shadow: var(--retro-flat-shadow);
|
||||
background-color: var(--retro-button-color);
|
||||
}
|
||||
|
||||
[ngComboboxInput]::placeholder {
|
||||
color: #000;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
[ngComboboxInput]:focus {
|
||||
outline: none;
|
||||
transform: translate(1px, 1px);
|
||||
box-shadow: var(--retro-pressed-shadow);
|
||||
}
|
||||
|
||||
[ngCombobox]:has([aria-expanded='false']) .popup {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.popup {
|
||||
width: 100%;
|
||||
margin-top: 20px;
|
||||
padding: 0.5rem;
|
||||
max-height: 11rem;
|
||||
border-radius: 0;
|
||||
background-color: var(--septenary-contrast);
|
||||
box-shadow: var(--retro-flat-shadow);
|
||||
}
|
||||
|
||||
.no-results {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
[ngListbox] {
|
||||
gap: 2px;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
overflow: auto;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
[ngOption] {
|
||||
display: flex;
|
||||
cursor: pointer;
|
||||
align-items: center;
|
||||
margin: 1px;
|
||||
padding: 0 1rem;
|
||||
min-height: 2.25rem;
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
[ngOption]:hover {
|
||||
background-color: color-mix(in srgb, var(--primary-contrast) 5%, transparent);
|
||||
}
|
||||
|
||||
[ngOption][data-active='true'] {
|
||||
outline-offset: -2px;
|
||||
outline: 2px dashed var(--hot-pink);
|
||||
}
|
||||
|
||||
[ngOption][aria-selected='true'] {
|
||||
color: var(--hot-pink);
|
||||
background-color: color-mix(in srgb, var(--hot-pink) 5%, transparent);
|
||||
}
|
||||
|
||||
[ngOption]:not([aria-selected='true']) .check-icon {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.option-label {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.check-icon {
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
<div ngCombobox focusMode="manual">
|
||||
<div #origin class="retro-autocomplete">
|
||||
<span class="search-icon material-symbols-outlined">search</span>
|
||||
<input
|
||||
aria-label="Label dropdown"
|
||||
placeholder="Select a country"
|
||||
[(ngModel)]="query"
|
||||
ngComboboxInput
|
||||
/>
|
||||
</div>
|
||||
|
||||
<ng-template ngComboboxPopupContainer>
|
||||
<ng-template
|
||||
[cdkConnectedOverlay]="{origin, usePopover: 'inline', matchWidth: true}"
|
||||
[cdkConnectedOverlayOpen]="true"
|
||||
>
|
||||
<div class="popup">
|
||||
@if (countries().length === 0) {
|
||||
<div class="no-results">No results found</div>
|
||||
}
|
||||
|
||||
<div ngListbox>
|
||||
@for (country of countries(); track country) {
|
||||
<div ngOption [value]="country" [label]="country">
|
||||
<span class="option-label">{{country}}</span>
|
||||
<span class="check-icon material-symbols-outlined">check</span>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</ng-template>
|
||||
</ng-template>
|
||||
</div>
|
||||
|
|
@ -0,0 +1,261 @@
|
|||
import {Combobox, ComboboxInput, ComboboxPopupContainer} from '@angular/aria/combobox';
|
||||
import {Listbox, Option} from '@angular/aria/listbox';
|
||||
import {OverlayModule} from '@angular/cdk/overlay';
|
||||
import {
|
||||
afterRenderEffect,
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
computed,
|
||||
signal,
|
||||
viewChild,
|
||||
viewChildren,
|
||||
} from '@angular/core';
|
||||
import {FormsModule} from '@angular/forms';
|
||||
|
||||
@Component({
|
||||
selector: 'app-root',
|
||||
templateUrl: 'app.component.html',
|
||||
styleUrl: 'app.component.css',
|
||||
imports: [
|
||||
Combobox,
|
||||
ComboboxInput,
|
||||
ComboboxPopupContainer,
|
||||
Listbox,
|
||||
Option,
|
||||
OverlayModule,
|
||||
FormsModule,
|
||||
],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class App {
|
||||
/** The combobox listbox popup. */
|
||||
listbox = viewChild<Listbox<string>>(Listbox);
|
||||
|
||||
/** The options available in the listbox. */
|
||||
options = viewChildren<Option<string>>(Option);
|
||||
|
||||
/** A reference to the ng aria combobox. */
|
||||
combobox = viewChild<Combobox<string>>(Combobox);
|
||||
|
||||
/** The query string used to filter the list of countries. */
|
||||
query = signal('');
|
||||
|
||||
/** The list of countries filtered by the query. */
|
||||
countries = computed(() =>
|
||||
ALL_COUNTRIES.filter((country) => country.toLowerCase().startsWith(this.query().toLowerCase())),
|
||||
);
|
||||
|
||||
constructor() {
|
||||
// Scrolls to the active item when the active option changes.
|
||||
// The slight delay here is to ensure animations are done before scrolling.
|
||||
afterRenderEffect(() => {
|
||||
const option = this.options().find((opt) => opt.active());
|
||||
setTimeout(() => option?.element.scrollIntoView({block: 'nearest'}), 50);
|
||||
});
|
||||
|
||||
// Resets the listbox scroll position when the combobox is closed.
|
||||
afterRenderEffect(() => {
|
||||
if (!this.combobox()?.expanded()) {
|
||||
setTimeout(() => this.listbox()?.element.scrollTo(0, 0), 150);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const ALL_COUNTRIES = [
|
||||
'Afghanistan',
|
||||
'Albania',
|
||||
'Algeria',
|
||||
'Andorra',
|
||||
'Angola',
|
||||
'Antigua and Barbuda',
|
||||
'Argentina',
|
||||
'Armenia',
|
||||
'Australia',
|
||||
'Austria',
|
||||
'Azerbaijan',
|
||||
'Bahamas',
|
||||
'Bahrain',
|
||||
'Bangladesh',
|
||||
'Barbados',
|
||||
'Belarus',
|
||||
'Belgium',
|
||||
'Belize',
|
||||
'Benin',
|
||||
'Bhutan',
|
||||
'Bolivia',
|
||||
'Bosnia and Herzegovina',
|
||||
'Botswana',
|
||||
'Brazil',
|
||||
'Brunei',
|
||||
'Bulgaria',
|
||||
'Burkina Faso',
|
||||
'Burundi',
|
||||
'Cabo Verde',
|
||||
'Cambodia',
|
||||
'Cameroon',
|
||||
'Canada',
|
||||
'Central African Republic',
|
||||
'Chad',
|
||||
'Chile',
|
||||
'China',
|
||||
'Colombia',
|
||||
'Comoros',
|
||||
'Congo (Congo-Brazzaville)',
|
||||
'Costa Rica',
|
||||
"Côte d'Ivoire",
|
||||
'Croatia',
|
||||
'Cuba',
|
||||
'Cyprus',
|
||||
'Czechia (Czech Republic)',
|
||||
'Democratic Republic of the Congo',
|
||||
'Denmark',
|
||||
'Djibouti',
|
||||
'Dominica',
|
||||
'Dominican Republic',
|
||||
'Ecuador',
|
||||
'Egypt',
|
||||
'El Salvador',
|
||||
'Equatorial Guinea',
|
||||
'Eritrea',
|
||||
'Estonia',
|
||||
'Eswatini (fmr. ""Swaziland"")',
|
||||
'Ethiopia',
|
||||
'Fiji',
|
||||
'Finland',
|
||||
'France',
|
||||
'Gabon',
|
||||
'Gambia',
|
||||
'Georgia',
|
||||
'Germany',
|
||||
'Ghana',
|
||||
'Greece',
|
||||
'Grenada',
|
||||
'Guatemala',
|
||||
'Guinea',
|
||||
'Guinea-Bissau',
|
||||
'Guyana',
|
||||
'Haiti',
|
||||
'Holy See',
|
||||
'Honduras',
|
||||
'Hungary',
|
||||
'Iceland',
|
||||
'India',
|
||||
'Indonesia',
|
||||
'Iran',
|
||||
'Iraq',
|
||||
'Ireland',
|
||||
'Israel',
|
||||
'Italy',
|
||||
'Jamaica',
|
||||
'Japan',
|
||||
'Jordan',
|
||||
'Kazakhstan',
|
||||
'Kenya',
|
||||
'Kiribati',
|
||||
'Kuwait',
|
||||
'Kyrgyzstan',
|
||||
'Laos',
|
||||
'Latvia',
|
||||
'Lebanon',
|
||||
'Lesotho',
|
||||
'Liberia',
|
||||
'Libya',
|
||||
'Liechtenstein',
|
||||
'Lithuania',
|
||||
'Luxembourg',
|
||||
'Madagascar',
|
||||
'Malawi',
|
||||
'Malaysia',
|
||||
'Maldives',
|
||||
'Mali',
|
||||
'Malta',
|
||||
'Marshall Islands',
|
||||
'Mauritania',
|
||||
'Mauritius',
|
||||
'Mexico',
|
||||
'Micronesia',
|
||||
'Moldova',
|
||||
'Monaco',
|
||||
'Mongolia',
|
||||
'Montenegro',
|
||||
'Morocco',
|
||||
'Mozambique',
|
||||
'Myanmar (formerly Burma)',
|
||||
'Namibia',
|
||||
'Nauru',
|
||||
'Nepal',
|
||||
'Netherlands',
|
||||
'New Zealand',
|
||||
'Nicaragua',
|
||||
'Niger',
|
||||
'Nigeria',
|
||||
'North Korea',
|
||||
'North Macedonia',
|
||||
'Norway',
|
||||
'Oman',
|
||||
'Pakistan',
|
||||
'Palau',
|
||||
'Palestine State',
|
||||
'Panama',
|
||||
'Papua New Guinea',
|
||||
'Paraguay',
|
||||
'Peru',
|
||||
'Philippines',
|
||||
'Poland',
|
||||
'Portugal',
|
||||
'Qatar',
|
||||
'Romania',
|
||||
'Russia',
|
||||
'Rwanda',
|
||||
'Saint Kitts and Nevis',
|
||||
'Saint Lucia',
|
||||
'Saint Vincent and the Grenadines',
|
||||
'Samoa',
|
||||
'San Marino',
|
||||
'Sao Tome and Principe',
|
||||
'Saudi Arabia',
|
||||
'Senegal',
|
||||
'Serbia',
|
||||
'Seychelles',
|
||||
'Sierra Leone',
|
||||
'Singapore',
|
||||
'Slovakia',
|
||||
'Slovenia',
|
||||
'Solomon Islands',
|
||||
'Somalia',
|
||||
'South Africa',
|
||||
'South Korea',
|
||||
'South Sudan',
|
||||
'Spain',
|
||||
'Sri Lanka',
|
||||
'Sudan',
|
||||
'Suriname',
|
||||
'Sweden',
|
||||
'Switzerland',
|
||||
'Syria',
|
||||
'Tajikistan',
|
||||
'Tanzania',
|
||||
'Thailand',
|
||||
'Timor-Leste',
|
||||
'Togo',
|
||||
'Tonga',
|
||||
'Trinidad and Tobago',
|
||||
'Tunisia',
|
||||
'Turkey',
|
||||
'Turkmenistan',
|
||||
'Tuvalu',
|
||||
'Uganda',
|
||||
'Ukraine',
|
||||
'United Arab Emirates',
|
||||
'United Kingdom',
|
||||
'United States of America',
|
||||
'Uruguay',
|
||||
'Uzbekistan',
|
||||
'Vanuatu',
|
||||
'Venezuela',
|
||||
'Vietnam',
|
||||
'Yemen',
|
||||
'Zambia',
|
||||
'Zimbabwe',
|
||||
];
|
||||
|
|
@ -5,11 +5,31 @@
|
|||
|
||||
An accessible input field that filters and suggests options as users type, helping them find and select values from a list.
|
||||
|
||||
<docs-code-multifile preview hideCode path="adev/src/content/examples/aria/autocomplete/src/basic/app/app.component.ts">
|
||||
<docs-code header="app.component.ts" path="adev/src/content/examples/aria/autocomplete/src/basic/app/app.component.ts"/>
|
||||
<docs-code header="app.component.html" path="adev/src/content/examples/aria/autocomplete/src/basic/app/app.component.html"/>
|
||||
<docs-code header="app.component.css" path="adev/src/content/examples/aria/autocomplete/src/basic/app/app.component.css"/>
|
||||
</docs-code-multifile>
|
||||
<docs-tab-group>
|
||||
<docs-tab label="Basic">
|
||||
<docs-code-multifile preview hideCode path="adev/src/content/examples/aria/autocomplete/src/basic/app/app.component.ts">
|
||||
<docs-code header="app.component.ts" path="adev/src/content/examples/aria/autocomplete/src/basic/app/app.component.ts"/>
|
||||
<docs-code header="app.component.html" path="adev/src/content/examples/aria/autocomplete/src/basic/app/app.component.html"/>
|
||||
<docs-code header="app.component.css" path="adev/src/content/examples/aria/autocomplete/src/basic/app/app.component.css"/>
|
||||
</docs-code-multifile>
|
||||
</docs-tab>
|
||||
|
||||
<docs-tab label="Material">
|
||||
<docs-code-multifile preview hideCode path="adev/src/content/examples/aria/autocomplete/src/basic/material/app/app.component.ts">
|
||||
<docs-code header="app.component.ts" path="adev/src/content/examples/aria/autocomplete/src/basic/material/app/app.component.ts"/>
|
||||
<docs-code header="app.component.html" path="adev/src/content/examples/aria/autocomplete/src/basic/material/app/app.component.html"/>
|
||||
<docs-code header="app.component.css" path="adev/src/content/examples/aria/autocomplete/src/basic/material/app/app.component.css"/>
|
||||
</docs-code-multifile>
|
||||
</docs-tab>
|
||||
|
||||
<docs-tab label="Retro">
|
||||
<docs-code-multifile preview hideCode path="adev/src/content/examples/aria/autocomplete/src/basic/retro/app/app.component.ts">
|
||||
<docs-code header="app.component.ts" path="adev/src/content/examples/aria/autocomplete/src/basic/retro/app/app.component.ts"/>
|
||||
<docs-code header="app.component.html" path="adev/src/content/examples/aria/autocomplete/src/basic/retro/app/app.component.html"/>
|
||||
<docs-code header="app.component.css" path="adev/src/content/examples/aria/autocomplete/src/basic/retro/app/app.component.css"/>
|
||||
</docs-code-multifile>
|
||||
</docs-tab>
|
||||
</docs-tab-group>
|
||||
|
||||
## Usage
|
||||
|
||||
|
|
@ -43,26 +63,91 @@ Angular's autocomplete provides a fully accessible combobox implementation with:
|
|||
|
||||
Users typing partial text expect immediate confirmation that their input matches an available option. Auto-select mode updates the input value to match the first filtered option as users type, reducing the number of keystrokes needed and providing instant feedback that their search is on the right track.
|
||||
|
||||
<docs-code-multifile preview hideCode path="adev/src/content/examples/aria/autocomplete/src/basic/app/app.component.ts">
|
||||
<docs-code header="app.component.ts" path="adev/src/content/examples/aria/autocomplete/src/basic/app/app.component.ts" visibleLines="[1,7,33,40]"/>
|
||||
<docs-code header="app.component.html" path="adev/src/content/examples/aria/autocomplete/src/basic/app/app.component.html"/>
|
||||
</docs-code-multifile>
|
||||
<docs-tab-group>
|
||||
<docs-tab label="Basic">
|
||||
<docs-code-multifile preview hideCode path="adev/src/content/examples/aria/autocomplete/src/basic/app/app.component.ts">
|
||||
<docs-code header="app.component.ts" path="adev/src/content/examples/aria/autocomplete/src/basic/app/app.component.ts"/>
|
||||
<docs-code header="app.component.html" path="adev/src/content/examples/aria/autocomplete/src/basic/app/app.component.html"/>
|
||||
<docs-code header="app.component.css" path="adev/src/content/examples/aria/autocomplete/src/basic/app/app.component.css"/>
|
||||
</docs-code-multifile>
|
||||
</docs-tab>
|
||||
|
||||
<docs-tab label="Material">
|
||||
<docs-code-multifile preview hideCode path="adev/src/content/examples/aria/autocomplete/src/basic/material/app/app.component.ts">
|
||||
<docs-code header="app.component.ts" path="adev/src/content/examples/aria/autocomplete/src/basic/material/app/app.component.ts"/>
|
||||
<docs-code header="app.component.html" path="adev/src/content/examples/aria/autocomplete/src/basic/material/app/app.component.html"/>
|
||||
<docs-code header="app.component.css" path="adev/src/content/examples/aria/autocomplete/src/basic/material/app/app.component.css"/>
|
||||
</docs-code-multifile>
|
||||
</docs-tab>
|
||||
|
||||
<docs-tab label="Retro">
|
||||
<docs-code-multifile preview hideCode path="adev/src/content/examples/aria/autocomplete/src/basic/retro/app/app.component.ts">
|
||||
<docs-code header="app.component.ts" path="adev/src/content/examples/aria/autocomplete/src/basic/retro/app/app.component.ts"/>
|
||||
<docs-code header="app.component.html" path="adev/src/content/examples/aria/autocomplete/src/basic/retro/app/app.component.html"/>
|
||||
<docs-code header="app.component.css" path="adev/src/content/examples/aria/autocomplete/src/basic/retro/app/app.component.css"/>
|
||||
</docs-code-multifile>
|
||||
</docs-tab>
|
||||
</docs-tab-group>
|
||||
|
||||
### Manual selection mode
|
||||
|
||||
Manual selection mode keeps the typed text unchanged while users navigate the suggestion list, preventing confusion from automatic updates. The input only changes when users explicitly confirm their choice with Enter or a click.
|
||||
|
||||
<docs-code-multifile preview hideCode path="adev/src/content/examples/aria/autocomplete/src/manual/app/app.component.ts">
|
||||
<docs-code header="app.component.html" path="adev/src/content/examples/aria/autocomplete/src/manual/app/app.component.html" visibleLines="[1]"/>
|
||||
</docs-code-multifile>
|
||||
<docs-tab-group>
|
||||
<docs-tab label="Basic">
|
||||
<docs-code-multifile preview hideCode path="adev/src/content/examples/aria/autocomplete/src/manual/app/app.component.ts">
|
||||
<docs-code header="app.component.ts" path="adev/src/content/examples/aria/autocomplete/src/manual/app/app.component.ts"/>
|
||||
<docs-code header="app.component.html" path="adev/src/content/examples/aria/autocomplete/src/manual/app/app.component.html"/>
|
||||
<docs-code header="app.component.css" path="adev/src/content/examples/aria/autocomplete/src/manual/app/app.component.css"/>
|
||||
</docs-code-multifile>
|
||||
</docs-tab>
|
||||
|
||||
<docs-tab label="Material">
|
||||
<docs-code-multifile preview hideCode path="adev/src/content/examples/aria/autocomplete/src/manual/material/app/app.component.ts">
|
||||
<docs-code header="app.component.ts" path="adev/src/content/examples/aria/autocomplete/src/manual/material/app/app.component.ts"/>
|
||||
<docs-code header="app.component.html" path="adev/src/content/examples/aria/autocomplete/src/manual/material/app/app.component.html"/>
|
||||
<docs-code header="app.component.css" path="adev/src/content/examples/aria/autocomplete/src/manual/material/app/app.component.css"/>
|
||||
</docs-code-multifile>
|
||||
</docs-tab>
|
||||
|
||||
<docs-tab label="Retro">
|
||||
<docs-code-multifile preview hideCode path="adev/src/content/examples/aria/autocomplete/src/manual/retro/app/app.component.ts">
|
||||
<docs-code header="app.component.ts" path="adev/src/content/examples/aria/autocomplete/src/manual/retro/app/app.component.ts"/>
|
||||
<docs-code header="app.component.html" path="adev/src/content/examples/aria/autocomplete/src/manual/retro/app/app.component.html"/>
|
||||
<docs-code header="app.component.css" path="adev/src/content/examples/aria/autocomplete/src/manual/retro/app/app.component.css"/>
|
||||
</docs-code-multifile>
|
||||
</docs-tab>
|
||||
</docs-tab-group>
|
||||
|
||||
### Highlight mode
|
||||
|
||||
Highlight mode allows the user to navigate options with arrow keys without changing the input value as they browse until they explicitly select a new option with Enter or click.
|
||||
|
||||
<docs-code-multifile preview hideCode path="adev/src/content/examples/aria/autocomplete/src/highlight/app/app.component.ts">
|
||||
<docs-code header="app.component.html" path="adev/src/content/examples/aria/autocomplete/src/highlight/app/app.component.html" visibleLines="[1]"/>
|
||||
</docs-code-multifile>
|
||||
<docs-tab-group>
|
||||
<docs-tab label="Basic">
|
||||
<docs-code-multifile preview hideCode path="adev/src/content/examples/aria/autocomplete/src/highlight/app/app.component.ts">
|
||||
<docs-code header="app.component.ts" path="adev/src/content/examples/aria/autocomplete/src/highlight/app/app.component.ts"/>
|
||||
<docs-code header="app.component.html" path="adev/src/content/examples/aria/autocomplete/src/highlight/app/app.component.html"/>
|
||||
<docs-code header="app.component.css" path="adev/src/content/examples/aria/autocomplete/src/highlight/app/app.component.css"/>
|
||||
</docs-code-multifile>
|
||||
</docs-tab>
|
||||
|
||||
<docs-tab label="Material">
|
||||
<docs-code-multifile preview hideCode path="adev/src/content/examples/aria/autocomplete/src/highlight/material/app/app.component.ts">
|
||||
<docs-code header="app.component.ts" path="adev/src/content/examples/aria/autocomplete/src/highlight/material/app/app.component.ts"/>
|
||||
<docs-code header="app.component.html" path="adev/src/content/examples/aria/autocomplete/src/highlight/material/app/app.component.html"/>
|
||||
<docs-code header="app.component.css" path="adev/src/content/examples/aria/autocomplete/src/highlight/material/app/app.component.css"/>
|
||||
</docs-code-multifile>
|
||||
</docs-tab>
|
||||
|
||||
<docs-tab label="Retro">
|
||||
<docs-code-multifile preview hideCode path="adev/src/content/examples/aria/autocomplete/src/highlight/retro/app/app.component.ts">
|
||||
<docs-code header="app.component.ts" path="adev/src/content/examples/aria/autocomplete/src/highlight/retro/app/app.component.ts"/>
|
||||
<docs-code header="app.component.html" path="adev/src/content/examples/aria/autocomplete/src/highlight/retro/app/app.component.html"/>
|
||||
<docs-code header="app.component.css" path="adev/src/content/examples/aria/autocomplete/src/highlight/retro/app/app.component.css"/>
|
||||
</docs-code-multifile>
|
||||
</docs-tab>
|
||||
</docs-tab-group>
|
||||
|
||||
## Showcase
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue