mirror of
https://github.com/angular/angular
synced 2026-05-24 09:28:37 +00:00
docs: modernize autocomplete examples and guide to signal apis
Some checks are pending
DevInfra / assistant_to_the_branch_manager (push) Waiting to run
CI (push) / lint (push) Waiting to run
CI (push) / zone-js (push) Waiting to run
CI (push) / devtools (push) Waiting to run
CI (push) / test (push) Waiting to run
CI (push) / integration-tests (push) Waiting to run
CI (push) / adev (push) Waiting to run
CI (push) / vscode-ng-language-service (push) Waiting to run
CI (push) / publish-snapshots (push) Waiting to run
CI (push) / adev-deploy (push) Blocked by required conditions
Update ADEV Cross Repo Docs / Update Cross Repo ADEV Docs (push) Waiting to run
Performance Tracking / workflow (push) Blocked by required conditions
Performance Tracking / list (push) Waiting to run
OpenSSF Scorecard / Scorecards analysis (push) Waiting to run
Some checks are pending
DevInfra / assistant_to_the_branch_manager (push) Waiting to run
CI (push) / lint (push) Waiting to run
CI (push) / zone-js (push) Waiting to run
CI (push) / devtools (push) Waiting to run
CI (push) / test (push) Waiting to run
CI (push) / integration-tests (push) Waiting to run
CI (push) / adev (push) Waiting to run
CI (push) / vscode-ng-language-service (push) Waiting to run
CI (push) / publish-snapshots (push) Waiting to run
CI (push) / adev-deploy (push) Blocked by required conditions
Update ADEV Cross Repo Docs / Update Cross Repo ADEV Docs (push) Waiting to run
Performance Tracking / workflow (push) Blocked by required conditions
Performance Tracking / list (push) Waiting to run
OpenSSF Scorecard / Scorecards analysis (push) Waiting to run
This commit is contained in:
parent
fd6ef34ac9
commit
f84653605a
29 changed files with 3939 additions and 29 deletions
|
|
@ -0,0 +1,89 @@
|
|||
html {
|
||||
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 {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.combobox-input {
|
||||
width: 100%;
|
||||
padding: 10px 12px;
|
||||
border: 1px solid #404040;
|
||||
border-radius: 4px;
|
||||
font-size: 16px;
|
||||
box-sizing: border-box;
|
||||
background-color: #1a1a1a;
|
||||
color: #e0e0e0;
|
||||
}
|
||||
|
||||
.combobox-input::placeholder {
|
||||
color: #888;
|
||||
}
|
||||
|
||||
.combobox-input:focus {
|
||||
outline: none;
|
||||
border-color: #4a9eff;
|
||||
background-color: #1f1f1f;
|
||||
}
|
||||
|
||||
.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;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
color: #e0e0e0;
|
||||
}
|
||||
|
||||
.option:hover {
|
||||
background-color: #2a2a2a;
|
||||
}
|
||||
|
||||
.option[data-active] {
|
||||
background-color: #2d4a6e;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.option[aria-selected='true'] {
|
||||
background-color: #4a9eff;
|
||||
color: #000000;
|
||||
}
|
||||
|
||||
.info {
|
||||
margin: 20px;
|
||||
padding: 16px;
|
||||
background-color: #1f1f1f;
|
||||
border-radius: 4px;
|
||||
border-left: 4px solid #4a9eff;
|
||||
color: #e0e0e0;
|
||||
}
|
||||
|
||||
.info p {
|
||||
margin: 8px 0;
|
||||
}
|
||||
|
|
@ -0,0 +1,113 @@
|
|||
@import url('https://fonts.googleapis.com/icon?family=Material+Symbols+Outlined');
|
||||
|
||||
:host {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
font-family: var(--inter-font);
|
||||
}
|
||||
|
||||
.autocomplete-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.autocomplete-input-container {
|
||||
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);
|
||||
}
|
||||
|
||||
.autocomplete-input {
|
||||
width: 13rem;
|
||||
font-size: 1rem;
|
||||
border-radius: 0.25rem;
|
||||
padding: 0.75rem 0.5rem 0.75rem 2.5rem;
|
||||
color: var(--primary-contrast);
|
||||
outline: none;
|
||||
border: 1px solid var(--quinary-contrast);
|
||||
background-color: var(--page-background);
|
||||
}
|
||||
|
||||
.autocomplete-input:focus-visible {
|
||||
border-color: var(--hot-pink);
|
||||
box-shadow: 0 0 0 2px color-mix(in srgb, var(--hot-pink) 20%, transparent);
|
||||
}
|
||||
|
||||
.autocomplete-input::placeholder {
|
||||
color: var(--quaternary-contrast);
|
||||
}
|
||||
|
||||
.popup {
|
||||
width: 100%;
|
||||
margin-top: 8px;
|
||||
padding: 0.5rem;
|
||||
max-height: 11rem;
|
||||
border-radius: 0.5rem;
|
||||
background-color: var(--septenary-contrast);
|
||||
font-size: 0.9rem;
|
||||
box-shadow:
|
||||
0 4px 6px -1px rgba(0, 0, 0, 0.1),
|
||||
0 2px 4px -1px rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
|
||||
.no-results {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.listbox {
|
||||
gap: 2px;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
overflow: auto;
|
||||
flex-direction: column;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.option {
|
||||
display: flex;
|
||||
cursor: pointer;
|
||||
align-items: center;
|
||||
margin: 1px;
|
||||
padding: 0 1rem;
|
||||
min-height: 2.25rem;
|
||||
border-radius: 0.5rem;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.option:hover {
|
||||
background-color: color-mix(in srgb, var(--primary-contrast) 5%, transparent);
|
||||
}
|
||||
|
||||
.option[data-active='true'] {
|
||||
outline-offset: -2px;
|
||||
outline: 2px solid var(--hot-pink);
|
||||
}
|
||||
|
||||
.option[aria-selected='true'] {
|
||||
color: var(--hot-pink);
|
||||
background-color: color-mix(in srgb, var(--hot-pink) 5%, transparent);
|
||||
}
|
||||
|
||||
.option:not([aria-selected='true']) .check-icon {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.option-label {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.check-icon {
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
|
@ -0,0 +1,55 @@
|
|||
<div class="autocomplete-container">
|
||||
<div #origin class="autocomplete-input-container">
|
||||
<span class="search-icon material-symbols-outlined" translate="no" aria-hidden="true"
|
||||
>search</span
|
||||
>
|
||||
<input
|
||||
#combobox="ngCombobox"
|
||||
ngCombobox
|
||||
class="autocomplete-input"
|
||||
placeholder="Select a country"
|
||||
[(value)]="query"
|
||||
[(expanded)]="popupExpanded"
|
||||
(click)="popupExpanded.set(true)"
|
||||
(focusout)="onBlur()"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div aria-live="polite" class="cdk-visually-hidden">
|
||||
{{ countries().length === 0 ? 'No results found for ' + query() : '' }}
|
||||
</div>
|
||||
|
||||
<ng-template
|
||||
[cdkConnectedOverlay]="{origin, usePopover: 'inline', matchWidth: true}"
|
||||
[cdkConnectedOverlayOpen]="popupExpanded()"
|
||||
>
|
||||
<ng-template ngComboboxPopup [combobox]="combobox">
|
||||
<div class="popup">
|
||||
@if (countries().length === 0) {
|
||||
<div class="no-results">No results found</div>
|
||||
}
|
||||
<div
|
||||
#listbox="ngListbox"
|
||||
ngListbox
|
||||
ngComboboxWidget
|
||||
class="listbox"
|
||||
focusMode="activedescendant"
|
||||
[tabindex]="-1"
|
||||
[activeDescendant]="listbox.activeDescendant()"
|
||||
[(value)]="selectedOption"
|
||||
(click)="onCommit()"
|
||||
(keydown.enter)="onCommit()"
|
||||
>
|
||||
@for (country of countries(); track country) {
|
||||
<div class="option" ngOption [value]="country" [label]="country">
|
||||
<span class="option-label">{{ country }}</span>
|
||||
<span class="check-icon material-symbols-outlined" translate="no" aria-hidden="true"
|
||||
>check</span
|
||||
>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</ng-template>
|
||||
</ng-template>
|
||||
</div>
|
||||
249
adev/src/content/examples/aria/autocomplete/src/basic/app/app.ts
Normal file
249
adev/src/content/examples/aria/autocomplete/src/basic/app/app.ts
Normal file
|
|
@ -0,0 +1,249 @@
|
|||
import {Combobox, ComboboxPopup, ComboboxWidget} from '@angular/aria/combobox';
|
||||
import {Listbox, Option} from '@angular/aria/listbox';
|
||||
import {OverlayModule} from '@angular/cdk/overlay';
|
||||
import {afterRenderEffect, Component, computed, signal, viewChild} from '@angular/core';
|
||||
import {FormsModule} from '@angular/forms';
|
||||
|
||||
@Component({
|
||||
selector: 'app-root[theme="basic-basic"], app-root:not([theme])',
|
||||
templateUrl: 'app.html',
|
||||
styleUrl: 'app.css',
|
||||
imports: [Combobox, ComboboxPopup, ComboboxWidget, Listbox, Option, OverlayModule, FormsModule],
|
||||
})
|
||||
export class App {
|
||||
readonly listbox = viewChild(Listbox);
|
||||
readonly combobox = viewChild(Combobox);
|
||||
|
||||
popupExpanded = signal(false);
|
||||
query = signal('');
|
||||
selectedOption = signal<string[]>([]);
|
||||
|
||||
countries = computed(() =>
|
||||
ALL_COUNTRIES.filter((country) => country.toLowerCase().startsWith(this.query().toLowerCase())),
|
||||
);
|
||||
|
||||
constructor() {
|
||||
afterRenderEffect(() => {
|
||||
if (this.combobox()?.expanded() === true) {
|
||||
this.listbox()?.scrollActiveItemIntoView();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
onBlur() {
|
||||
this.commitSelection();
|
||||
}
|
||||
|
||||
onCommit() {
|
||||
this.commitSelection();
|
||||
this.popupExpanded.set(false);
|
||||
}
|
||||
|
||||
private commitSelection() {
|
||||
const selected = this.selectedOption();
|
||||
if (selected.length > 0) {
|
||||
this.query.set(selected[0]);
|
||||
} else {
|
||||
this.query.set('');
|
||||
this.selectedOption.set([]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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,114 @@
|
|||
@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);
|
||||
}
|
||||
|
||||
.autocomplete-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.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);
|
||||
}
|
||||
|
||||
.autocomplete-input {
|
||||
width: 13rem;
|
||||
font-size: 1rem;
|
||||
border-radius: 3rem;
|
||||
padding: 0.75rem 0.5rem 0.75rem 2.5rem;
|
||||
color: var(--primary-contrast);
|
||||
outline: none;
|
||||
border: 1px solid var(--quinary-contrast);
|
||||
background-color: var(--page-background);
|
||||
}
|
||||
|
||||
.autocomplete-input:focus-visible {
|
||||
border-color: var(--primary);
|
||||
box-shadow: 0 0 0 2px color-mix(in srgb, var(--primary) 20%, transparent);
|
||||
}
|
||||
|
||||
.autocomplete-input::placeholder {
|
||||
color: var(--quaternary-contrast);
|
||||
}
|
||||
|
||||
.popup {
|
||||
width: 100%;
|
||||
margin-top: 8px;
|
||||
padding: 0.5rem;
|
||||
max-height: 11rem;
|
||||
border-radius: 2rem;
|
||||
background-color: var(--septenary-contrast);
|
||||
font-size: 0.9rem;
|
||||
box-shadow:
|
||||
0 4px 6px -1px rgba(0, 0, 0, 0.1),
|
||||
0 2px 4px -1px rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
|
||||
.no-results {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.listbox {
|
||||
gap: 2px;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
overflow: auto;
|
||||
flex-direction: column;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.option {
|
||||
display: flex;
|
||||
cursor: pointer;
|
||||
align-items: center;
|
||||
margin: 1px;
|
||||
padding: 0 1rem;
|
||||
min-height: 3rem;
|
||||
border-radius: 3rem;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.option:hover {
|
||||
background-color: color-mix(in srgb, var(--primary-contrast) 5%, transparent);
|
||||
}
|
||||
|
||||
.option[data-active='true'] {
|
||||
outline-offset: -2px;
|
||||
outline: 2px solid var(--primary);
|
||||
}
|
||||
|
||||
.option[aria-selected='true'] {
|
||||
color: var(--primary);
|
||||
background-color: color-mix(in srgb, var(--primary) 10%, transparent);
|
||||
}
|
||||
|
||||
.option:not([aria-selected='true']) .check-icon {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.option-label {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.check-icon {
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
|
@ -0,0 +1,55 @@
|
|||
<div class="autocomplete-container">
|
||||
<div #origin class="material-autocomplete">
|
||||
<span class="search-icon material-symbols-outlined" translate="no" aria-hidden="true"
|
||||
>search</span
|
||||
>
|
||||
<input
|
||||
#combobox="ngCombobox"
|
||||
ngCombobox
|
||||
class="autocomplete-input"
|
||||
placeholder="Select a country"
|
||||
[(value)]="query"
|
||||
[(expanded)]="popupExpanded"
|
||||
(click)="popupExpanded.set(true)"
|
||||
(focusout)="onBlur()"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div aria-live="polite" class="cdk-visually-hidden">
|
||||
{{ countries().length === 0 ? 'No results found for ' + query() : '' }}
|
||||
</div>
|
||||
|
||||
<ng-template
|
||||
[cdkConnectedOverlay]="{origin, usePopover: 'inline', matchWidth: true}"
|
||||
[cdkConnectedOverlayOpen]="popupExpanded()"
|
||||
>
|
||||
<ng-template ngComboboxPopup [combobox]="combobox">
|
||||
<div class="popup">
|
||||
@if (countries().length === 0) {
|
||||
<div class="no-results">No results found</div>
|
||||
}
|
||||
<div
|
||||
#listbox="ngListbox"
|
||||
ngListbox
|
||||
ngComboboxWidget
|
||||
class="listbox"
|
||||
focusMode="activedescendant"
|
||||
[tabindex]="-1"
|
||||
[activeDescendant]="listbox.activeDescendant()"
|
||||
[(value)]="selectedOption"
|
||||
(click)="onCommit()"
|
||||
(keydown.enter)="onCommit()"
|
||||
>
|
||||
@for (country of countries(); track country) {
|
||||
<div class="option" ngOption [value]="country" [label]="country">
|
||||
<span class="option-label">{{ country }}</span>
|
||||
<span class="check-icon material-symbols-outlined" translate="no" aria-hidden="true"
|
||||
>check</span
|
||||
>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</ng-template>
|
||||
</ng-template>
|
||||
</div>
|
||||
|
|
@ -0,0 +1,249 @@
|
|||
import {Combobox, ComboboxPopup, ComboboxWidget} from '@angular/aria/combobox';
|
||||
import {Listbox, Option} from '@angular/aria/listbox';
|
||||
import {OverlayModule} from '@angular/cdk/overlay';
|
||||
import {afterRenderEffect, Component, computed, signal, viewChild} from '@angular/core';
|
||||
import {FormsModule} from '@angular/forms';
|
||||
|
||||
@Component({
|
||||
selector: 'app-root[theme="basic-material"]',
|
||||
templateUrl: 'app.html',
|
||||
styleUrl: 'app.css',
|
||||
imports: [Combobox, ComboboxPopup, ComboboxWidget, Listbox, Option, OverlayModule, FormsModule],
|
||||
})
|
||||
export class App {
|
||||
readonly listbox = viewChild(Listbox);
|
||||
readonly combobox = viewChild(Combobox);
|
||||
|
||||
popupExpanded = signal(false);
|
||||
query = signal('');
|
||||
selectedOption = signal<string[]>([]);
|
||||
|
||||
countries = computed(() =>
|
||||
ALL_COUNTRIES.filter((country) => country.toLowerCase().startsWith(this.query().toLowerCase())),
|
||||
);
|
||||
|
||||
constructor() {
|
||||
afterRenderEffect(() => {
|
||||
if (this.combobox()?.expanded() === true) {
|
||||
this.listbox()?.scrollActiveItemIntoView();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
onBlur() {
|
||||
this.commitSelection();
|
||||
}
|
||||
|
||||
onCommit() {
|
||||
this.commitSelection();
|
||||
this.popupExpanded.set(false);
|
||||
}
|
||||
|
||||
private commitSelection() {
|
||||
const selected = this.selectedOption();
|
||||
if (selected.length > 0) {
|
||||
this.query.set(selected[0]);
|
||||
} else {
|
||||
this.query.set('');
|
||||
this.selectedOption.set([]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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,135 @@
|
|||
@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(--tertiary-contrast),
|
||||
0px 4px 0px 0px var(--tertiary-contrast), -4px 0px 0px 0px var(--tertiary-contrast),
|
||||
0px -4px 0px 0px var(--tertiary-contrast);
|
||||
--retro-flat-shadow:
|
||||
4px 0px 0px 0px var(--tertiary-contrast), 0px 4px 0px 0px var(--tertiary-contrast),
|
||||
-4px 0px 0px 0px var(--tertiary-contrast), 0px -4px 0px 0px var(--tertiary-contrast);
|
||||
--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(--tertiary-contrast),
|
||||
0px 4px 0px 0px var(--tertiary-contrast), -4px 0px 0px 0px var(--tertiary-contrast),
|
||||
0px -4px 0px 0px var(--tertiary-contrast), 0px 0px 0px 0px var(--tertiary-contrast);
|
||||
}
|
||||
|
||||
.autocomplete-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
|
||||
.autocomplete-input {
|
||||
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);
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.autocomplete-input:focus-visible {
|
||||
outline: none;
|
||||
transform: translate(1px, 1px);
|
||||
box-shadow: var(--retro-pressed-shadow);
|
||||
}
|
||||
|
||||
.autocomplete-input::placeholder {
|
||||
color: #000;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
|
||||
.listbox {
|
||||
gap: 2px;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
overflow: auto;
|
||||
flex-direction: column;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.option {
|
||||
display: flex;
|
||||
cursor: pointer;
|
||||
align-items: center;
|
||||
margin: 1px;
|
||||
padding: 0 1rem;
|
||||
min-height: 2.25rem;
|
||||
border-radius: 0;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.option:hover {
|
||||
background-color: color-mix(in srgb, var(--primary-contrast) 5%, transparent);
|
||||
}
|
||||
|
||||
.option[data-active='true'] {
|
||||
outline-offset: -2px;
|
||||
outline: 2px dashed var(--hot-pink);
|
||||
}
|
||||
|
||||
.option[aria-selected='true'] {
|
||||
color: var(--hot-pink);
|
||||
background-color: color-mix(in srgb, var(--hot-pink) 5%, transparent);
|
||||
}
|
||||
|
||||
.option:not([aria-selected='true']) .check-icon {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.option-label {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.check-icon {
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
|
@ -0,0 +1,55 @@
|
|||
<div class="autocomplete-container">
|
||||
<div #origin class="retro-autocomplete">
|
||||
<span class="search-icon material-symbols-outlined" translate="no" aria-hidden="true"
|
||||
>search</span
|
||||
>
|
||||
<input
|
||||
#combobox="ngCombobox"
|
||||
ngCombobox
|
||||
class="autocomplete-input"
|
||||
placeholder="Select a country"
|
||||
[(value)]="query"
|
||||
[(expanded)]="popupExpanded"
|
||||
(click)="popupExpanded.set(true)"
|
||||
(focusout)="onBlur()"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div aria-live="polite" class="cdk-visually-hidden">
|
||||
{{ countries().length === 0 ? 'No results found for ' + query() : '' }}
|
||||
</div>
|
||||
|
||||
<ng-template
|
||||
[cdkConnectedOverlay]="{origin, usePopover: 'inline', matchWidth: true}"
|
||||
[cdkConnectedOverlayOpen]="popupExpanded()"
|
||||
>
|
||||
<ng-template ngComboboxPopup [combobox]="combobox">
|
||||
<div class="popup">
|
||||
@if (countries().length === 0) {
|
||||
<div class="no-results">No results found</div>
|
||||
}
|
||||
<div
|
||||
#listbox="ngListbox"
|
||||
ngListbox
|
||||
ngComboboxWidget
|
||||
class="listbox"
|
||||
focusMode="activedescendant"
|
||||
[tabindex]="-1"
|
||||
[activeDescendant]="listbox.activeDescendant()"
|
||||
[(value)]="selectedOption"
|
||||
(click)="onCommit()"
|
||||
(keydown.enter)="onCommit()"
|
||||
>
|
||||
@for (country of countries(); track country) {
|
||||
<div class="option" ngOption [value]="country" [label]="country">
|
||||
<span class="option-label">{{ country }}</span>
|
||||
<span class="check-icon material-symbols-outlined" translate="no" aria-hidden="true"
|
||||
>check</span
|
||||
>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</ng-template>
|
||||
</ng-template>
|
||||
</div>
|
||||
|
|
@ -0,0 +1,249 @@
|
|||
import {Combobox, ComboboxPopup, ComboboxWidget} from '@angular/aria/combobox';
|
||||
import {Listbox, Option} from '@angular/aria/listbox';
|
||||
import {OverlayModule} from '@angular/cdk/overlay';
|
||||
import {afterRenderEffect, Component, computed, signal, viewChild} from '@angular/core';
|
||||
import {FormsModule} from '@angular/forms';
|
||||
|
||||
@Component({
|
||||
selector: 'app-root[theme="basic-retro"]',
|
||||
templateUrl: 'app.html',
|
||||
styleUrl: 'app.css',
|
||||
imports: [Combobox, ComboboxPopup, ComboboxWidget, Listbox, Option, OverlayModule, FormsModule],
|
||||
})
|
||||
export class App {
|
||||
readonly listbox = viewChild(Listbox);
|
||||
readonly combobox = viewChild(Combobox);
|
||||
|
||||
popupExpanded = signal(false);
|
||||
query = signal('');
|
||||
selectedOption = signal<string[]>([]);
|
||||
|
||||
countries = computed(() =>
|
||||
ALL_COUNTRIES.filter((country) => country.toLowerCase().startsWith(this.query().toLowerCase())),
|
||||
);
|
||||
|
||||
constructor() {
|
||||
afterRenderEffect(() => {
|
||||
if (this.combobox()?.expanded() === true) {
|
||||
this.listbox()?.scrollActiveItemIntoView();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
onBlur() {
|
||||
this.commitSelection();
|
||||
}
|
||||
|
||||
onCommit() {
|
||||
this.commitSelection();
|
||||
this.popupExpanded.set(false);
|
||||
}
|
||||
|
||||
private commitSelection() {
|
||||
const selected = this.selectedOption();
|
||||
if (selected.length > 0) {
|
||||
this.query.set(selected[0]);
|
||||
} else {
|
||||
this.query.set('');
|
||||
this.selectedOption.set([]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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,113 @@
|
|||
@import url('https://fonts.googleapis.com/icon?family=Material+Symbols+Outlined');
|
||||
|
||||
:host {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
font-family: var(--inter-font);
|
||||
}
|
||||
|
||||
.autocomplete-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.autocomplete-input-container {
|
||||
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);
|
||||
}
|
||||
|
||||
.autocomplete-input {
|
||||
width: 13rem;
|
||||
font-size: 1rem;
|
||||
border-radius: 0.25rem;
|
||||
padding: 0.75rem 0.5rem 0.75rem 2.5rem;
|
||||
color: var(--primary-contrast);
|
||||
outline: none;
|
||||
border: 1px solid var(--quinary-contrast);
|
||||
background-color: var(--page-background);
|
||||
}
|
||||
|
||||
.autocomplete-input:focus-visible {
|
||||
border-color: var(--hot-pink);
|
||||
box-shadow: 0 0 0 2px color-mix(in srgb, var(--hot-pink) 20%, transparent);
|
||||
}
|
||||
|
||||
.autocomplete-input::placeholder {
|
||||
color: var(--quaternary-contrast);
|
||||
}
|
||||
|
||||
.popup {
|
||||
width: 100%;
|
||||
margin-top: 8px;
|
||||
padding: 0.5rem;
|
||||
max-height: 11rem;
|
||||
border-radius: 0.5rem;
|
||||
background-color: var(--septenary-contrast);
|
||||
font-size: 0.9rem;
|
||||
box-shadow:
|
||||
0 4px 6px -1px rgba(0, 0, 0, 0.1),
|
||||
0 2px 4px -1px rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
|
||||
.no-results {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.listbox {
|
||||
gap: 2px;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
overflow: auto;
|
||||
flex-direction: column;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.option {
|
||||
display: flex;
|
||||
cursor: pointer;
|
||||
align-items: center;
|
||||
margin: 1px;
|
||||
padding: 0 1rem;
|
||||
min-height: 2.25rem;
|
||||
border-radius: 0.5rem;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.option:hover {
|
||||
background-color: color-mix(in srgb, var(--primary-contrast) 5%, transparent);
|
||||
}
|
||||
|
||||
.option[data-active='true'] {
|
||||
outline-offset: -2px;
|
||||
outline: 2px solid var(--hot-pink);
|
||||
}
|
||||
|
||||
.option[aria-selected='true'] {
|
||||
color: var(--hot-pink);
|
||||
background-color: color-mix(in srgb, var(--hot-pink) 5%, transparent);
|
||||
}
|
||||
|
||||
.option:not([aria-selected='true']) .check-icon {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.option-label {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.check-icon {
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
|
@ -0,0 +1,57 @@
|
|||
<div class="autocomplete-container">
|
||||
<div #origin class="autocomplete-input-container">
|
||||
<span class="search-icon material-symbols-outlined" translate="no" aria-hidden="true"
|
||||
>search</span
|
||||
>
|
||||
<input
|
||||
#combobox="ngCombobox"
|
||||
ngCombobox
|
||||
class="autocomplete-input"
|
||||
placeholder="Select a country"
|
||||
[(value)]="query"
|
||||
[(expanded)]="popupExpanded"
|
||||
(click)="popupExpanded.set(true)"
|
||||
(keydown.arrowdown)="navigated.set(true)"
|
||||
(keydown.arrowup)="navigated.set(true)"
|
||||
[inlineSuggestion]="query() || navigated() ? selectedOption()[0] : undefined"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div aria-live="polite" class="cdk-visually-hidden">
|
||||
{{ countries().length === 0 ? 'No results found for ' + query() : '' }}
|
||||
</div>
|
||||
|
||||
<ng-template
|
||||
[cdkConnectedOverlay]="{origin, usePopover: 'inline', matchWidth: true}"
|
||||
[cdkConnectedOverlayOpen]="popupExpanded()"
|
||||
>
|
||||
<ng-template ngComboboxPopup [combobox]="combobox">
|
||||
<div class="popup">
|
||||
@if (countries().length === 0) {
|
||||
<div class="no-results">No results found</div>
|
||||
}
|
||||
<div
|
||||
#listbox="ngListbox"
|
||||
ngListbox
|
||||
ngComboboxWidget
|
||||
class="listbox"
|
||||
focusMode="activedescendant"
|
||||
[tabindex]="-1"
|
||||
[activeDescendant]="listbox.activeDescendant()"
|
||||
[(value)]="selectedOption"
|
||||
(click)="onCommit()"
|
||||
(keydown.enter)="onCommit()"
|
||||
>
|
||||
@for (country of countries(); track country) {
|
||||
<div class="option" ngOption [value]="country" [label]="country">
|
||||
<span class="option-label">{{ country }}</span>
|
||||
<span class="check-icon material-symbols-outlined" translate="no" aria-hidden="true"
|
||||
>check</span
|
||||
>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</ng-template>
|
||||
</ng-template>
|
||||
</div>
|
||||
|
|
@ -0,0 +1,247 @@
|
|||
import {Combobox, ComboboxPopup, ComboboxWidget} from '@angular/aria/combobox';
|
||||
import {Listbox, Option} from '@angular/aria/listbox';
|
||||
import {OverlayModule} from '@angular/cdk/overlay';
|
||||
import {afterRenderEffect, Component, computed, effect, signal, viewChild} from '@angular/core';
|
||||
import {FormsModule} from '@angular/forms';
|
||||
|
||||
@Component({
|
||||
selector: 'app-root[theme="highlight-basic"], app-root:not([theme])',
|
||||
templateUrl: 'app.html',
|
||||
styleUrl: 'app.css',
|
||||
imports: [Combobox, ComboboxPopup, ComboboxWidget, Listbox, Option, OverlayModule, FormsModule],
|
||||
})
|
||||
export class App {
|
||||
readonly listbox = viewChild(Listbox);
|
||||
readonly combobox = viewChild(Combobox);
|
||||
|
||||
popupExpanded = signal(false);
|
||||
query = signal('');
|
||||
selectedOption = signal<string[]>([]);
|
||||
navigated = signal(false);
|
||||
|
||||
countries = computed(() =>
|
||||
ALL_COUNTRIES.filter((country) => country.toLowerCase().startsWith(this.query().toLowerCase())),
|
||||
);
|
||||
|
||||
constructor() {
|
||||
afterRenderEffect(() => {
|
||||
if (this.combobox()?.expanded() === true) {
|
||||
this.listbox()?.scrollActiveItemIntoView();
|
||||
}
|
||||
});
|
||||
|
||||
effect(() => {
|
||||
if (!this.popupExpanded()) {
|
||||
this.navigated.set(false);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
onCommit() {
|
||||
const selected = this.selectedOption();
|
||||
if (selected.length > 0) {
|
||||
this.query.set(selected[0]);
|
||||
} else {
|
||||
this.query.set('');
|
||||
}
|
||||
this.popupExpanded.set(false);
|
||||
}
|
||||
}
|
||||
|
||||
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,114 @@
|
|||
@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);
|
||||
}
|
||||
|
||||
.autocomplete-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.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);
|
||||
}
|
||||
|
||||
.autocomplete-input {
|
||||
width: 13rem;
|
||||
font-size: 1rem;
|
||||
border-radius: 3rem;
|
||||
padding: 0.75rem 0.5rem 0.75rem 2.5rem;
|
||||
color: var(--primary-contrast);
|
||||
outline: none;
|
||||
border: 1px solid var(--quinary-contrast);
|
||||
background-color: var(--page-background);
|
||||
}
|
||||
|
||||
.autocomplete-input:focus-visible {
|
||||
border-color: var(--primary);
|
||||
box-shadow: 0 0 0 2px color-mix(in srgb, var(--primary) 20%, transparent);
|
||||
}
|
||||
|
||||
.autocomplete-input::placeholder {
|
||||
color: var(--quaternary-contrast);
|
||||
}
|
||||
|
||||
.popup {
|
||||
width: 100%;
|
||||
margin-top: 8px;
|
||||
padding: 0.5rem;
|
||||
max-height: 11rem;
|
||||
border-radius: 2rem;
|
||||
background-color: var(--septenary-contrast);
|
||||
font-size: 0.9rem;
|
||||
box-shadow:
|
||||
0 4px 6px -1px rgba(0, 0, 0, 0.1),
|
||||
0 2px 4px -1px rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
|
||||
.no-results {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.listbox {
|
||||
gap: 2px;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
overflow: auto;
|
||||
flex-direction: column;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.option {
|
||||
display: flex;
|
||||
cursor: pointer;
|
||||
align-items: center;
|
||||
margin: 1px;
|
||||
padding: 0 1rem;
|
||||
min-height: 3rem;
|
||||
border-radius: 3rem;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.option:hover {
|
||||
background-color: color-mix(in srgb, var(--primary-contrast) 5%, transparent);
|
||||
}
|
||||
|
||||
.option[data-active='true'] {
|
||||
outline-offset: -2px;
|
||||
outline: 2px solid var(--primary);
|
||||
}
|
||||
|
||||
.option[aria-selected='true'] {
|
||||
color: var(--primary);
|
||||
background-color: color-mix(in srgb, var(--primary) 10%, transparent);
|
||||
}
|
||||
|
||||
.option:not([aria-selected='true']) .check-icon {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.option-label {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.check-icon {
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
|
@ -0,0 +1,57 @@
|
|||
<div class="autocomplete-container">
|
||||
<div #origin class="material-autocomplete">
|
||||
<span class="search-icon material-symbols-outlined" translate="no" aria-hidden="true"
|
||||
>search</span
|
||||
>
|
||||
<input
|
||||
#combobox="ngCombobox"
|
||||
ngCombobox
|
||||
class="autocomplete-input"
|
||||
placeholder="Select a country"
|
||||
[(value)]="query"
|
||||
[(expanded)]="popupExpanded"
|
||||
(click)="popupExpanded.set(true)"
|
||||
(keydown.arrowdown)="navigated.set(true)"
|
||||
(keydown.arrowup)="navigated.set(true)"
|
||||
[inlineSuggestion]="query() || navigated() ? selectedOption()[0] : undefined"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div aria-live="polite" class="cdk-visually-hidden">
|
||||
{{ countries().length === 0 ? 'No results found for ' + query() : '' }}
|
||||
</div>
|
||||
|
||||
<ng-template
|
||||
[cdkConnectedOverlay]="{origin, usePopover: 'inline', matchWidth: true}"
|
||||
[cdkConnectedOverlayOpen]="popupExpanded()"
|
||||
>
|
||||
<ng-template ngComboboxPopup [combobox]="combobox">
|
||||
<div class="popup">
|
||||
@if (countries().length === 0) {
|
||||
<div class="no-results">No results found</div>
|
||||
}
|
||||
<div
|
||||
#listbox="ngListbox"
|
||||
ngListbox
|
||||
ngComboboxWidget
|
||||
class="listbox"
|
||||
focusMode="activedescendant"
|
||||
[tabindex]="-1"
|
||||
[activeDescendant]="listbox.activeDescendant()"
|
||||
[(value)]="selectedOption"
|
||||
(click)="onCommit()"
|
||||
(keydown.enter)="onCommit()"
|
||||
>
|
||||
@for (country of countries(); track country) {
|
||||
<div class="option" ngOption [value]="country" [label]="country">
|
||||
<span class="option-label">{{ country }}</span>
|
||||
<span class="check-icon material-symbols-outlined" translate="no" aria-hidden="true"
|
||||
>check</span
|
||||
>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</ng-template>
|
||||
</ng-template>
|
||||
</div>
|
||||
|
|
@ -0,0 +1,247 @@
|
|||
import {Combobox, ComboboxPopup, ComboboxWidget} from '@angular/aria/combobox';
|
||||
import {Listbox, Option} from '@angular/aria/listbox';
|
||||
import {OverlayModule} from '@angular/cdk/overlay';
|
||||
import {afterRenderEffect, Component, computed, effect, signal, viewChild} from '@angular/core';
|
||||
import {FormsModule} from '@angular/forms';
|
||||
|
||||
@Component({
|
||||
selector: 'app-root[theme="highlight-material"]',
|
||||
templateUrl: 'app.html',
|
||||
styleUrl: 'app.css',
|
||||
imports: [Combobox, ComboboxPopup, ComboboxWidget, Listbox, Option, OverlayModule, FormsModule],
|
||||
})
|
||||
export class App {
|
||||
readonly listbox = viewChild(Listbox);
|
||||
readonly combobox = viewChild(Combobox);
|
||||
|
||||
popupExpanded = signal(false);
|
||||
query = signal('');
|
||||
selectedOption = signal<string[]>([]);
|
||||
navigated = signal(false);
|
||||
|
||||
countries = computed(() =>
|
||||
ALL_COUNTRIES.filter((country) => country.toLowerCase().startsWith(this.query().toLowerCase())),
|
||||
);
|
||||
|
||||
constructor() {
|
||||
afterRenderEffect(() => {
|
||||
if (this.combobox()?.expanded() === true) {
|
||||
this.listbox()?.scrollActiveItemIntoView();
|
||||
}
|
||||
});
|
||||
|
||||
effect(() => {
|
||||
if (!this.popupExpanded()) {
|
||||
this.navigated.set(false);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
onCommit() {
|
||||
const selected = this.selectedOption();
|
||||
if (selected.length > 0) {
|
||||
this.query.set(selected[0]);
|
||||
} else {
|
||||
this.query.set('');
|
||||
}
|
||||
this.popupExpanded.set(false);
|
||||
}
|
||||
}
|
||||
|
||||
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,135 @@
|
|||
@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(--tertiary-contrast),
|
||||
0px 4px 0px 0px var(--tertiary-contrast), -4px 0px 0px 0px var(--tertiary-contrast),
|
||||
0px -4px 0px 0px var(--tertiary-contrast);
|
||||
--retro-flat-shadow:
|
||||
4px 0px 0px 0px var(--tertiary-contrast), 0px 4px 0px 0px var(--tertiary-contrast),
|
||||
-4px 0px 0px 0px var(--tertiary-contrast), 0px -4px 0px 0px var(--tertiary-contrast);
|
||||
--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(--tertiary-contrast),
|
||||
0px 4px 0px 0px var(--tertiary-contrast), -4px 0px 0px 0px var(--tertiary-contrast),
|
||||
0px -4px 0px 0px var(--tertiary-contrast), 0px 0px 0px 0px var(--tertiary-contrast);
|
||||
}
|
||||
|
||||
.autocomplete-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
|
||||
.autocomplete-input {
|
||||
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);
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.autocomplete-input:focus-visible {
|
||||
outline: none;
|
||||
transform: translate(1px, 1px);
|
||||
box-shadow: var(--retro-pressed-shadow);
|
||||
}
|
||||
|
||||
.autocomplete-input::placeholder {
|
||||
color: #000;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
|
||||
.listbox {
|
||||
gap: 2px;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
overflow: auto;
|
||||
flex-direction: column;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.option {
|
||||
display: flex;
|
||||
cursor: pointer;
|
||||
align-items: center;
|
||||
margin: 1px;
|
||||
padding: 0 1rem;
|
||||
min-height: 2.25rem;
|
||||
border-radius: 0;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.option:hover {
|
||||
background-color: color-mix(in srgb, var(--primary-contrast) 5%, transparent);
|
||||
}
|
||||
|
||||
.option[data-active='true'] {
|
||||
outline-offset: -2px;
|
||||
outline: 2px dashed var(--hot-pink);
|
||||
}
|
||||
|
||||
.option[aria-selected='true'] {
|
||||
color: var(--hot-pink);
|
||||
background-color: color-mix(in srgb, var(--hot-pink) 5%, transparent);
|
||||
}
|
||||
|
||||
.option:not([aria-selected='true']) .check-icon {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.option-label {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.check-icon {
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
|
@ -0,0 +1,57 @@
|
|||
<div class="autocomplete-container">
|
||||
<div #origin class="retro-autocomplete">
|
||||
<span class="search-icon material-symbols-outlined" translate="no" aria-hidden="true"
|
||||
>search</span
|
||||
>
|
||||
<input
|
||||
#combobox="ngCombobox"
|
||||
ngCombobox
|
||||
class="autocomplete-input"
|
||||
placeholder="Select a country"
|
||||
[(value)]="query"
|
||||
[(expanded)]="popupExpanded"
|
||||
(click)="popupExpanded.set(true)"
|
||||
(keydown.arrowdown)="navigated.set(true)"
|
||||
(keydown.arrowup)="navigated.set(true)"
|
||||
[inlineSuggestion]="query() || navigated() ? selectedOption()[0] : undefined"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div aria-live="polite" class="cdk-visually-hidden">
|
||||
{{ countries().length === 0 ? 'No results found for ' + query() : '' }}
|
||||
</div>
|
||||
|
||||
<ng-template
|
||||
[cdkConnectedOverlay]="{origin, usePopover: 'inline', matchWidth: true}"
|
||||
[cdkConnectedOverlayOpen]="popupExpanded()"
|
||||
>
|
||||
<ng-template ngComboboxPopup [combobox]="combobox">
|
||||
<div class="popup">
|
||||
@if (countries().length === 0) {
|
||||
<div class="no-results">No results found</div>
|
||||
}
|
||||
<div
|
||||
#listbox="ngListbox"
|
||||
ngListbox
|
||||
ngComboboxWidget
|
||||
class="listbox"
|
||||
focusMode="activedescendant"
|
||||
[tabindex]="-1"
|
||||
[activeDescendant]="listbox.activeDescendant()"
|
||||
[(value)]="selectedOption"
|
||||
(click)="onCommit()"
|
||||
(keydown.enter)="onCommit()"
|
||||
>
|
||||
@for (country of countries(); track country) {
|
||||
<div class="option" ngOption [value]="country" [label]="country">
|
||||
<span class="option-label">{{ country }}</span>
|
||||
<span class="check-icon material-symbols-outlined" translate="no" aria-hidden="true"
|
||||
>check</span
|
||||
>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</ng-template>
|
||||
</ng-template>
|
||||
</div>
|
||||
|
|
@ -0,0 +1,248 @@
|
|||
import {Combobox, ComboboxPopup, ComboboxWidget} from '@angular/aria/combobox';
|
||||
import {Listbox, Option} from '@angular/aria/listbox';
|
||||
import {OverlayModule} from '@angular/cdk/overlay';
|
||||
import {afterRenderEffect, Component, computed, effect, signal, viewChild} from '@angular/core';
|
||||
import {FormsModule} from '@angular/forms';
|
||||
|
||||
@Component({
|
||||
selector: 'app-root[theme="highlight-retro"]',
|
||||
templateUrl: 'app.html',
|
||||
styleUrl: 'app.css',
|
||||
imports: [Combobox, ComboboxPopup, ComboboxWidget, Listbox, Option, OverlayModule, FormsModule],
|
||||
})
|
||||
export class App {
|
||||
readonly listbox = viewChild(Listbox);
|
||||
readonly combobox = viewChild(Combobox);
|
||||
|
||||
popupExpanded = signal(false);
|
||||
query = signal('');
|
||||
selectedOption = signal<string[]>([]);
|
||||
navigated = signal(false);
|
||||
|
||||
countries = computed(() =>
|
||||
ALL_COUNTRIES.filter((country) => country.toLowerCase().startsWith(this.query().toLowerCase())),
|
||||
);
|
||||
|
||||
constructor() {
|
||||
afterRenderEffect(() => {
|
||||
if (this.combobox()?.expanded() === true) {
|
||||
this.listbox()?.scrollActiveItemIntoView();
|
||||
}
|
||||
});
|
||||
|
||||
effect(() => {
|
||||
if (!this.popupExpanded()) {
|
||||
this.navigated.set(false);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
onCommit() {
|
||||
const selected = this.selectedOption();
|
||||
if (selected.length > 0) {
|
||||
this.query.set(selected[0]);
|
||||
} else {
|
||||
this.query.set('');
|
||||
}
|
||||
this.popupExpanded.set(false);
|
||||
}
|
||||
}
|
||||
|
||||
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',
|
||||
'Imporant',
|
||||
'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,113 @@
|
|||
@import url('https://fonts.googleapis.com/icon?family=Material+Symbols+Outlined');
|
||||
|
||||
:host {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
font-family: var(--inter-font);
|
||||
}
|
||||
|
||||
.autocomplete-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.autocomplete-input-container {
|
||||
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);
|
||||
}
|
||||
|
||||
.autocomplete-input {
|
||||
width: 13rem;
|
||||
font-size: 1rem;
|
||||
border-radius: 0.25rem;
|
||||
padding: 0.75rem 0.5rem 0.75rem 2.5rem;
|
||||
color: var(--primary-contrast);
|
||||
outline: none;
|
||||
border: 1px solid var(--quinary-contrast);
|
||||
background-color: var(--page-background);
|
||||
}
|
||||
|
||||
.autocomplete-input:focus-visible {
|
||||
border-color: var(--hot-pink);
|
||||
box-shadow: 0 0 0 2px color-mix(in srgb, var(--hot-pink) 20%, transparent);
|
||||
}
|
||||
|
||||
.autocomplete-input::placeholder {
|
||||
color: var(--quaternary-contrast);
|
||||
}
|
||||
|
||||
.popup {
|
||||
width: 100%;
|
||||
margin-top: 8px;
|
||||
padding: 0.5rem;
|
||||
max-height: 11rem;
|
||||
border-radius: 0.5rem;
|
||||
background-color: var(--septenary-contrast);
|
||||
font-size: 0.9rem;
|
||||
box-shadow:
|
||||
0 4px 6px -1px rgba(0, 0, 0, 0.1),
|
||||
0 2px 4px -1px rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
|
||||
.no-results {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.listbox {
|
||||
gap: 2px;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
overflow: auto;
|
||||
flex-direction: column;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.option {
|
||||
display: flex;
|
||||
cursor: pointer;
|
||||
align-items: center;
|
||||
margin: 1px;
|
||||
padding: 0 1rem;
|
||||
min-height: 2.25rem;
|
||||
border-radius: 0.5rem;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.option:hover {
|
||||
background-color: color-mix(in srgb, var(--primary-contrast) 5%, transparent);
|
||||
}
|
||||
|
||||
.option[data-active='true'] {
|
||||
outline-offset: -2px;
|
||||
outline: 2px solid var(--hot-pink);
|
||||
}
|
||||
|
||||
.option[aria-selected='true'] {
|
||||
color: var(--hot-pink);
|
||||
background-color: color-mix(in srgb, var(--hot-pink) 5%, transparent);
|
||||
}
|
||||
|
||||
.option:not([aria-selected='true']) .check-icon {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.option-label {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.check-icon {
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
|
@ -0,0 +1,55 @@
|
|||
<div class="autocomplete-container">
|
||||
<div #origin class="autocomplete-input-container">
|
||||
<span class="search-icon material-symbols-outlined" translate="no" aria-hidden="true"
|
||||
>search</span
|
||||
>
|
||||
<input
|
||||
#combobox="ngCombobox"
|
||||
ngCombobox
|
||||
class="autocomplete-input"
|
||||
placeholder="Select a country"
|
||||
[(value)]="query"
|
||||
[(expanded)]="popupExpanded"
|
||||
(click)="popupExpanded.set(true)"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div aria-live="polite" class="cdk-visually-hidden">
|
||||
{{ countries().length === 0 ? 'No results found for ' + query() : '' }}
|
||||
</div>
|
||||
|
||||
<ng-template
|
||||
[cdkConnectedOverlay]="{origin, usePopover: 'inline', matchWidth: true}"
|
||||
[cdkConnectedOverlayOpen]="popupExpanded()"
|
||||
>
|
||||
<ng-template ngComboboxPopup [combobox]="combobox">
|
||||
<div class="popup">
|
||||
@if (countries().length === 0) {
|
||||
<div class="no-results">No results found</div>
|
||||
}
|
||||
<div
|
||||
#listbox="ngListbox"
|
||||
ngListbox
|
||||
ngComboboxWidget
|
||||
class="listbox"
|
||||
focusMode="activedescendant"
|
||||
selectionMode="explicit"
|
||||
[tabindex]="-1"
|
||||
[activeDescendant]="listbox.activeDescendant()"
|
||||
[(value)]="selectedOption"
|
||||
(click)="onCommit()"
|
||||
(keydown.enter)="onCommit()"
|
||||
>
|
||||
@for (country of countries(); track country) {
|
||||
<div class="option" ngOption [value]="country" [label]="country">
|
||||
<span class="option-label">{{ country }}</span>
|
||||
<span class="check-icon material-symbols-outlined" translate="no" aria-hidden="true"
|
||||
>check</span
|
||||
>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</ng-template>
|
||||
</ng-template>
|
||||
</div>
|
||||
|
|
@ -0,0 +1,238 @@
|
|||
import {Combobox, ComboboxPopup, ComboboxWidget} from '@angular/aria/combobox';
|
||||
import {Listbox, Option} from '@angular/aria/listbox';
|
||||
import {OverlayModule} from '@angular/cdk/overlay';
|
||||
import {afterRenderEffect, Component, computed, signal, viewChild} from '@angular/core';
|
||||
import {FormsModule} from '@angular/forms';
|
||||
|
||||
@Component({
|
||||
selector: 'app-root[theme="manual-basic"], app-root:not([theme])',
|
||||
templateUrl: 'app.html',
|
||||
styleUrl: 'app.css',
|
||||
imports: [Combobox, ComboboxPopup, ComboboxWidget, Listbox, Option, OverlayModule, FormsModule],
|
||||
})
|
||||
export class App {
|
||||
readonly listbox = viewChild(Listbox);
|
||||
readonly combobox = viewChild(Combobox);
|
||||
|
||||
popupExpanded = signal(false);
|
||||
query = signal('');
|
||||
selectedOption = signal<string[]>([]);
|
||||
|
||||
countries = computed(() =>
|
||||
ALL_COUNTRIES.filter((country) => country.toLowerCase().startsWith(this.query().toLowerCase())),
|
||||
);
|
||||
|
||||
constructor() {
|
||||
afterRenderEffect(() => {
|
||||
if (this.combobox()?.expanded() === true) {
|
||||
this.listbox()?.scrollActiveItemIntoView();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
onCommit() {
|
||||
const selected = this.selectedOption();
|
||||
if (selected.length > 0) {
|
||||
this.query.set(selected[0]);
|
||||
}
|
||||
this.popupExpanded.set(false);
|
||||
}
|
||||
}
|
||||
|
||||
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,114 @@
|
|||
@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);
|
||||
}
|
||||
|
||||
.autocomplete-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.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);
|
||||
}
|
||||
|
||||
.autocomplete-input {
|
||||
width: 13rem;
|
||||
font-size: 1rem;
|
||||
border-radius: 3rem;
|
||||
padding: 0.75rem 0.5rem 0.75rem 2.5rem;
|
||||
color: var(--primary-contrast);
|
||||
outline: none;
|
||||
border: 1px solid var(--quinary-contrast);
|
||||
background-color: var(--page-background);
|
||||
}
|
||||
|
||||
.autocomplete-input:focus-visible {
|
||||
border-color: var(--primary);
|
||||
box-shadow: 0 0 0 2px color-mix(in srgb, var(--primary) 20%, transparent);
|
||||
}
|
||||
|
||||
.autocomplete-input::placeholder {
|
||||
color: var(--quaternary-contrast);
|
||||
}
|
||||
|
||||
.popup {
|
||||
width: 100%;
|
||||
margin-top: 8px;
|
||||
padding: 0.5rem;
|
||||
max-height: 11rem;
|
||||
border-radius: 2rem;
|
||||
background-color: var(--septenary-contrast);
|
||||
font-size: 0.9rem;
|
||||
box-shadow:
|
||||
0 4px 6px -1px rgba(0, 0, 0, 0.1),
|
||||
0 2px 4px -1px rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
|
||||
.no-results {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.listbox {
|
||||
gap: 2px;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
overflow: auto;
|
||||
flex-direction: column;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.option {
|
||||
display: flex;
|
||||
cursor: pointer;
|
||||
align-items: center;
|
||||
margin: 1px;
|
||||
padding: 0 1rem;
|
||||
min-height: 3rem;
|
||||
border-radius: 3rem;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.option:hover {
|
||||
background-color: color-mix(in srgb, var(--primary-contrast) 5%, transparent);
|
||||
}
|
||||
|
||||
.option[data-active='true'] {
|
||||
outline-offset: -2px;
|
||||
outline: 2px solid var(--primary);
|
||||
}
|
||||
|
||||
.option[aria-selected='true'] {
|
||||
color: var(--primary);
|
||||
background-color: color-mix(in srgb, var(--primary) 10%, transparent);
|
||||
}
|
||||
|
||||
.option:not([aria-selected='true']) .check-icon {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.option-label {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.check-icon {
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
|
@ -0,0 +1,55 @@
|
|||
<div class="autocomplete-container">
|
||||
<div #origin class="material-autocomplete">
|
||||
<span class="search-icon material-symbols-outlined" translate="no" aria-hidden="true"
|
||||
>search</span
|
||||
>
|
||||
<input
|
||||
#combobox="ngCombobox"
|
||||
ngCombobox
|
||||
class="autocomplete-input"
|
||||
placeholder="Select a country"
|
||||
[(value)]="query"
|
||||
[(expanded)]="popupExpanded"
|
||||
(click)="popupExpanded.set(true)"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div aria-live="polite" class="cdk-visually-hidden">
|
||||
{{ countries().length === 0 ? 'No results found for ' + query() : '' }}
|
||||
</div>
|
||||
|
||||
<ng-template
|
||||
[cdkConnectedOverlay]="{origin, usePopover: 'inline', matchWidth: true}"
|
||||
[cdkConnectedOverlayOpen]="popupExpanded()"
|
||||
>
|
||||
<ng-template ngComboboxPopup [combobox]="combobox">
|
||||
<div class="popup">
|
||||
@if (countries().length === 0) {
|
||||
<div class="no-results">No results found</div>
|
||||
}
|
||||
<div
|
||||
#listbox="ngListbox"
|
||||
ngListbox
|
||||
ngComboboxWidget
|
||||
class="listbox"
|
||||
focusMode="activedescendant"
|
||||
selectionMode="explicit"
|
||||
[tabindex]="-1"
|
||||
[activeDescendant]="listbox.activeDescendant()"
|
||||
[(value)]="selectedOption"
|
||||
(click)="onCommit()"
|
||||
(keydown.enter)="onCommit()"
|
||||
>
|
||||
@for (country of countries(); track country) {
|
||||
<div class="option" ngOption [value]="country" [label]="country">
|
||||
<span class="option-label">{{ country }}</span>
|
||||
<span class="check-icon material-symbols-outlined" translate="no" aria-hidden="true"
|
||||
>check</span
|
||||
>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</ng-template>
|
||||
</ng-template>
|
||||
</div>
|
||||
|
|
@ -0,0 +1,238 @@
|
|||
import {Combobox, ComboboxPopup, ComboboxWidget} from '@angular/aria/combobox';
|
||||
import {Listbox, Option} from '@angular/aria/listbox';
|
||||
import {OverlayModule} from '@angular/cdk/overlay';
|
||||
import {afterRenderEffect, Component, computed, signal, viewChild} from '@angular/core';
|
||||
import {FormsModule} from '@angular/forms';
|
||||
|
||||
@Component({
|
||||
selector: 'app-root[theme="manual-material"]',
|
||||
templateUrl: 'app.html',
|
||||
styleUrl: 'app.css',
|
||||
imports: [Combobox, ComboboxPopup, ComboboxWidget, Listbox, Option, OverlayModule, FormsModule],
|
||||
})
|
||||
export class App {
|
||||
readonly listbox = viewChild(Listbox);
|
||||
readonly combobox = viewChild(Combobox);
|
||||
|
||||
popupExpanded = signal(false);
|
||||
query = signal('');
|
||||
selectedOption = signal<string[]>([]);
|
||||
|
||||
countries = computed(() =>
|
||||
ALL_COUNTRIES.filter((country) => country.toLowerCase().startsWith(this.query().toLowerCase())),
|
||||
);
|
||||
|
||||
constructor() {
|
||||
afterRenderEffect(() => {
|
||||
if (this.combobox()?.expanded() === true) {
|
||||
this.listbox()?.scrollActiveItemIntoView();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
onCommit() {
|
||||
const selected = this.selectedOption();
|
||||
if (selected.length > 0) {
|
||||
this.query.set(selected[0]);
|
||||
}
|
||||
this.popupExpanded.set(false);
|
||||
}
|
||||
}
|
||||
|
||||
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,135 @@
|
|||
@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(--tertiary-contrast),
|
||||
0px 4px 0px 0px var(--tertiary-contrast), -4px 0px 0px 0px var(--tertiary-contrast),
|
||||
0px -4px 0px 0px var(--tertiary-contrast);
|
||||
--retro-flat-shadow:
|
||||
4px 0px 0px 0px var(--tertiary-contrast), 0px 4px 0px 0px var(--tertiary-contrast),
|
||||
-4px 0px 0px 0px var(--tertiary-contrast), 0px -4px 0px 0px var(--tertiary-contrast);
|
||||
--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(--tertiary-contrast),
|
||||
0px 4px 0px 0px var(--tertiary-contrast), -4px 0px 0px 0px var(--tertiary-contrast),
|
||||
0px -4px 0px 0px var(--tertiary-contrast), 0px 0px 0px 0px var(--tertiary-contrast);
|
||||
}
|
||||
|
||||
.autocomplete-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
|
||||
.autocomplete-input {
|
||||
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);
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.autocomplete-input:focus-visible {
|
||||
outline: none;
|
||||
transform: translate(1px, 1px);
|
||||
box-shadow: var(--retro-pressed-shadow);
|
||||
}
|
||||
|
||||
.autocomplete-input::placeholder {
|
||||
color: #000;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
|
||||
.listbox {
|
||||
gap: 2px;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
overflow: auto;
|
||||
flex-direction: column;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.option {
|
||||
display: flex;
|
||||
cursor: pointer;
|
||||
align-items: center;
|
||||
margin: 1px;
|
||||
padding: 0 1rem;
|
||||
min-height: 2.25rem;
|
||||
border-radius: 0;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.option:hover {
|
||||
background-color: color-mix(in srgb, var(--primary-contrast) 5%, transparent);
|
||||
}
|
||||
|
||||
.option[data-active='true'] {
|
||||
outline-offset: -2px;
|
||||
outline: 2px dashed var(--hot-pink);
|
||||
}
|
||||
|
||||
.option[aria-selected='true'] {
|
||||
color: var(--hot-pink);
|
||||
background-color: color-mix(in srgb, var(--hot-pink) 5%, transparent);
|
||||
}
|
||||
|
||||
.option:not([aria-selected='true']) .check-icon {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.option-label {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.check-icon {
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
|
@ -0,0 +1,55 @@
|
|||
<div class="autocomplete-container">
|
||||
<div #origin class="retro-autocomplete">
|
||||
<span class="search-icon material-symbols-outlined" translate="no" aria-hidden="true"
|
||||
>search</span
|
||||
>
|
||||
<input
|
||||
#combobox="ngCombobox"
|
||||
ngCombobox
|
||||
class="autocomplete-input"
|
||||
placeholder="Select a country"
|
||||
[(value)]="query"
|
||||
[(expanded)]="popupExpanded"
|
||||
(click)="popupExpanded.set(true)"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div aria-live="polite" class="cdk-visually-hidden">
|
||||
{{ countries().length === 0 ? 'No results found for ' + query() : '' }}
|
||||
</div>
|
||||
|
||||
<ng-template
|
||||
[cdkConnectedOverlay]="{origin, usePopover: 'inline', matchWidth: true}"
|
||||
[cdkConnectedOverlayOpen]="popupExpanded()"
|
||||
>
|
||||
<ng-template ngComboboxPopup [combobox]="combobox">
|
||||
<div class="popup">
|
||||
@if (countries().length === 0) {
|
||||
<div class="no-results">No results found</div>
|
||||
}
|
||||
<div
|
||||
#listbox="ngListbox"
|
||||
ngListbox
|
||||
ngComboboxWidget
|
||||
class="listbox"
|
||||
focusMode="activedescendant"
|
||||
selectionMode="explicit"
|
||||
[tabindex]="-1"
|
||||
[activeDescendant]="listbox.activeDescendant()"
|
||||
[(value)]="selectedOption"
|
||||
(click)="onCommit()"
|
||||
(keydown.enter)="onCommit()"
|
||||
>
|
||||
@for (country of countries(); track country) {
|
||||
<div class="option" ngOption [value]="country" [label]="country">
|
||||
<span class="option-label">{{ country }}</span>
|
||||
<span class="check-icon material-symbols-outlined" translate="no" aria-hidden="true"
|
||||
>check</span
|
||||
>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</ng-template>
|
||||
</ng-template>
|
||||
</div>
|
||||
|
|
@ -0,0 +1,238 @@
|
|||
import {Combobox, ComboboxPopup, ComboboxWidget} from '@angular/aria/combobox';
|
||||
import {Listbox, Option} from '@angular/aria/listbox';
|
||||
import {OverlayModule} from '@angular/cdk/overlay';
|
||||
import {afterRenderEffect, Component, computed, signal, viewChild} from '@angular/core';
|
||||
import {FormsModule} from '@angular/forms';
|
||||
|
||||
@Component({
|
||||
selector: 'app-root[theme="manual-retro"]',
|
||||
templateUrl: 'app.html',
|
||||
styleUrl: 'app.css',
|
||||
imports: [Combobox, ComboboxPopup, ComboboxWidget, Listbox, Option, OverlayModule, FormsModule],
|
||||
})
|
||||
export class App {
|
||||
readonly listbox = viewChild(Listbox);
|
||||
readonly combobox = viewChild(Combobox);
|
||||
|
||||
popupExpanded = signal(false);
|
||||
query = signal('');
|
||||
selectedOption = signal<string[]>([]);
|
||||
|
||||
countries = computed(() =>
|
||||
ALL_COUNTRIES.filter((country) => country.toLowerCase().startsWith(this.query().toLowerCase())),
|
||||
);
|
||||
|
||||
constructor() {
|
||||
afterRenderEffect(() => {
|
||||
if (this.combobox()?.expanded() === true) {
|
||||
this.listbox()?.scrollActiveItemIntoView();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
onCommit() {
|
||||
const selected = this.selectedOption();
|
||||
if (selected.length > 0) {
|
||||
this.query.set(selected[0]);
|
||||
}
|
||||
this.popupExpanded.set(false);
|
||||
}
|
||||
}
|
||||
|
||||
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,7 +5,7 @@
|
|||
|
||||
An accessible input field that filters and suggests options as users type, helping them find and select values from a list.
|
||||
|
||||
<!-- <docs-tab-group>
|
||||
<docs-tab-group>
|
||||
<docs-tab label="Basic">
|
||||
<docs-code-multifile preview hideCode path="adev/src/content/examples/aria/autocomplete/src/basic/app/app.ts">
|
||||
<docs-code header="app.ts" path="adev/src/content/examples/aria/autocomplete/src/basic/app/app.ts"/>
|
||||
|
|
@ -29,7 +29,7 @@ An accessible input field that filters and suggests options as users type, helpi
|
|||
<docs-code header="app.css" path="adev/src/content/examples/aria/autocomplete/src/basic/retro/app/app.css"/>
|
||||
</docs-code-multifile>
|
||||
</docs-tab>
|
||||
</docs-tab-group> -->
|
||||
</docs-tab-group>
|
||||
|
||||
## Usage
|
||||
|
||||
|
|
@ -52,7 +52,7 @@ Angular's autocomplete provides a fully accessible combobox implementation with:
|
|||
|
||||
- **Keyboard Navigation** - Navigate options with arrow keys, select with Enter, close with Escape
|
||||
- **Screen Reader Support** - Built-in ARIA attributes for assistive technologies
|
||||
- **Three Filter Modes** - Choose between auto-select, manual selection, or highlighting behavior
|
||||
- **Dynamic Highlight Behavior** - Built-in support for inline selection suggestions
|
||||
- **Signal-Based Reactivity** - Reactive state management using Angular signals
|
||||
- **Popover API Integration** - Leverages the native HTML Popover API for optimal positioning
|
||||
- **Bidirectional Text Support** - Automatically handles right-to-left (RTL) languages
|
||||
|
|
@ -63,7 +63,7 @@ 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-tab-group>
|
||||
<docs-tab-group>
|
||||
<docs-tab label="Basic">
|
||||
<docs-code-multifile preview hideCode path="adev/src/content/examples/aria/autocomplete/src/basic/app/app.ts">
|
||||
<docs-code header="app.ts" path="adev/src/content/examples/aria/autocomplete/src/basic/app/app.ts"/>
|
||||
|
|
@ -87,13 +87,13 @@ Users typing partial text expect immediate confirmation that their input matches
|
|||
<docs-code header="app.css" path="adev/src/content/examples/aria/autocomplete/src/basic/retro/app/app.css"/>
|
||||
</docs-code-multifile>
|
||||
</docs-tab>
|
||||
</docs-tab-group> -->
|
||||
</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-tab-group>
|
||||
<docs-tab-group>
|
||||
<docs-tab label="Basic">
|
||||
<docs-code-multifile preview hideCode path="adev/src/content/examples/aria/autocomplete/src/manual/app/app.ts">
|
||||
<docs-code header="app.ts" path="adev/src/content/examples/aria/autocomplete/src/manual/app/app.ts"/>
|
||||
|
|
@ -117,13 +117,13 @@ Manual selection mode keeps the typed text unchanged while users navigate the su
|
|||
<docs-code header="app.css" path="adev/src/content/examples/aria/autocomplete/src/manual/retro/app/app.css"/>
|
||||
</docs-code-multifile>
|
||||
</docs-tab>
|
||||
</docs-tab-group> -->
|
||||
</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-tab-group>
|
||||
<docs-tab-group>
|
||||
<docs-tab label="Basic">
|
||||
<docs-code-multifile preview hideCode path="adev/src/content/examples/aria/autocomplete/src/highlight/app/app.ts">
|
||||
<docs-code header="app.ts" path="adev/src/content/examples/aria/autocomplete/src/highlight/app/app.ts"/>
|
||||
|
|
@ -147,44 +147,75 @@ Highlight mode allows the user to navigate options with arrow keys without chang
|
|||
<docs-code header="app.css" path="adev/src/content/examples/aria/autocomplete/src/highlight/retro/app/app.css"/>
|
||||
</docs-code-multifile>
|
||||
</docs-tab>
|
||||
</docs-tab-group> -->
|
||||
</docs-tab-group>
|
||||
|
||||
## APIs
|
||||
|
||||
### Combobox Directive
|
||||
|
||||
The `ngCombobox` directive provides the container for autocomplete functionality.
|
||||
The `ngCombobox` directive is applied directly onto the editable text `<input>` or `<textarea>` to manage keyboard triggers and popover states.
|
||||
|
||||
#### Inputs
|
||||
|
||||
| Property | Type | Default | Description |
|
||||
| ------------ | ---------------------------------------------- | ---------- | ------------------------------------------------- |
|
||||
| `filterMode` | `'auto-select'` \| `'manual'` \| `'highlight'` | `'manual'` | Controls selection behavior |
|
||||
| `disabled` | `boolean` | `false` | Disables the combobox |
|
||||
| `firstMatch` | `string` | - | The value of the first matching item in the popup |
|
||||
| Property | Type | Default | Description |
|
||||
| ------------------ | --------------------- | ----------- | --------------------------------------------------------------- |
|
||||
| `disabled` | `boolean` | `false` | Disables the combobox |
|
||||
| `softDisabled` | `boolean` | `true` | Focusable when disabled |
|
||||
| `inlineSuggestion` | `string \| undefined` | `undefined` | Displays an inline completion suggestion for autocomplete modes |
|
||||
|
||||
#### Outputs
|
||||
#### Models
|
||||
|
||||
| Property | Type | Description |
|
||||
| ---------- | ----------------- | ----------------------------------------------------- |
|
||||
| `expanded` | `Signal<boolean>` | Signal indicating whether the popup is currently open |
|
||||
| Property | Type | Default | Description |
|
||||
| ---------- | ---------------------- | ------- | ----------------------------------------------------------------- |
|
||||
| `value` | `ModelSignal<string>` | `''` | Two-way bindable value of the input using `[(value)]` |
|
||||
| `expanded` | `ModelSignal<boolean>` | `false` | Two-way bindable expanded state of the popup using `[(expanded)]` |
|
||||
|
||||
### ComboboxInput Directive
|
||||
---
|
||||
|
||||
The `ngComboboxInput` directive connects an input element to the combobox.
|
||||
### ComboboxPopup Directive
|
||||
|
||||
#### Model
|
||||
A structural directive applied to `<ng-template>` to mark the container used as the popup.
|
||||
|
||||
| Property | Type | Description |
|
||||
| -------- | -------- | ------------------------------------------------------------ |
|
||||
| `value` | `string` | Two-way bindable string value of the input using `[(value)]` |
|
||||
#### Inputs
|
||||
|
||||
### ComboboxPopupContainer Directive
|
||||
| Property | Type | Description |
|
||||
| ---------- | ---------- | ------------------------------------------- |
|
||||
| `combobox` | `Combobox` | Required reference to the parent `Combobox` |
|
||||
|
||||
The `ngComboboxPopupContainer` directive wraps the popup content and manages its display.
|
||||
---
|
||||
|
||||
Must be used with `<ng-template>` inside a popover element.
|
||||
### ComboboxWidget Directive
|
||||
|
||||
Applied to the popup content container to bridge active-descendant focus changes to the input trigger.
|
||||
|
||||
#### Inputs
|
||||
|
||||
| Property | Type | Description |
|
||||
| ------------------ | --------------------- | --------------------------------------------------------------------------------- |
|
||||
| `activeDescendant` | `string \| undefined` | The ID of the currently active descendant (bound to `listbox.activeDescendant()`) |
|
||||
|
||||
---
|
||||
|
||||
### Listbox Directives
|
||||
|
||||
Autocomplete suggestion lists use the standard standalone listbox directives.
|
||||
|
||||
#### Inputs
|
||||
|
||||
| Property | Type | Default | Description |
|
||||
| --------------- | ---------------------------------- | ---------- | ---------------------------------------------------------------------------------------------------- |
|
||||
| `selectionMode` | `'follow'` \| `'explicit'` | `'follow'` | In manual/explicit mode, updates are committed explicitly on click/Enter rather than following focus |
|
||||
| `focusMode` | `'roving'` \| `'activedescendant'` | `'roving'` | Set to `'activedescendant'` so browser focus stays on the trigger input |
|
||||
| `tabIndex` | `number` | `0` | Set to `-1` to prevent keyboard tab focus from entering the popup listbox container |
|
||||
|
||||
#### Models
|
||||
|
||||
| Property | Type | Description |
|
||||
| -------- | -------------------- | ----------------------------------------------------------- |
|
||||
| `value` | `ModelSignal<any[]>` | Two-way bindable array of selected values using `[(value)]` |
|
||||
|
||||
---
|
||||
|
||||
### Related components
|
||||
|
||||
Autocomplete uses [Listbox](/api/aria/listbox/Listbox) and [Option](/api/aria/listbox/Option) directives to render the suggestion list. See the [Listbox documentation](/guide/aria/listbox) for additional customization options.
|
||||
Autocomplete uses standard standalone [Listbox](/api/aria/listbox/Listbox) and [Option](/api/aria/listbox/Option) directives. See the [Listbox documentation](/guide/aria/listbox) for advanced options.
|
||||
|
|
|
|||
Loading…
Reference in a new issue