コンボボックス
概要
テキスト入力とポップアップを連携させ、オートコンプリート、セレクト、マルチセレクトのパターンにプリミティブディレクティブを提供するディレクティブです。
app.component.ts
import {Combobox, ComboboxInput, ComboboxPopupContainer} from '@angular/aria/combobox';import {Listbox, Option} from '@angular/aria/listbox';import {OverlayModule} from '@angular/cdk/overlay';import { afterRenderEffect, ChangeDetectionStrategy, Component, computed, signal, viewChild, viewChildren,} from '@angular/core';import {FormsModule} from '@angular/forms';@Component({ selector: 'app-root', templateUrl: 'app.component.html', styleUrl: 'app.component.css', imports: [ Combobox, ComboboxInput, ComboboxPopupContainer, Listbox, Option, OverlayModule, FormsModule, ], changeDetection: ChangeDetectionStrategy.OnPush,})export class App { /** The combobox listbox popup. */ listbox = viewChild<Listbox<string>>(Listbox); /** The options available in the listbox. */ options = viewChildren<Option<string>>(Option); /** A reference to the ng aria combobox. */ combobox = viewChild<Combobox<string>>(Combobox); /** The query string used to filter the list of countries. */ query = signal(''); /** The list of countries filtered by the query. */ countries = computed(() => ALL_COUNTRIES.filter((country) => country.toLowerCase().startsWith(this.query().toLowerCase())), ); constructor() { // Scrolls to the active item when the active option changes. // The slight delay here is to ensure animations are done before scrolling. afterRenderEffect(() => { const option = this.options().find((opt) => opt.active()); setTimeout(() => option?.element.scrollIntoView({block: 'nearest'}), 50); }); // Resets the listbox scroll position when the combobox is closed. afterRenderEffect(() => { if (!this.combobox()?.expanded()) { setTimeout(() => this.listbox()?.element.scrollTo(0, 0), 150); } }); }}const ALL_COUNTRIES = [ 'Afghanistan', 'Albania', 'Algeria', 'Andorra', 'Angola', 'Antigua and Barbuda', 'Argentina', 'Armenia', 'Australia', 'Austria', 'Azerbaijan', 'Bahamas', 'Bahrain', 'Bangladesh', 'Barbados', 'Belarus', 'Belgium', 'Belize', 'Benin', 'Bhutan', 'Bolivia', 'Bosnia and Herzegovina', 'Botswana', 'Brazil', 'Brunei', 'Bulgaria', 'Burkina Faso', 'Burundi', 'Cabo Verde', 'Cambodia', 'Cameroon', 'Canada', 'Central African Republic', 'Chad', 'Chile', 'China', 'Colombia', 'Comoros', 'Congo (Congo-Brazzaville)', 'Costa Rica', "Côte d'Ivoire", 'Croatia', 'Cuba', 'Cyprus', 'Czechia (Czech Republic)', 'Democratic Republic of the Congo', 'Denmark', 'Djibouti', 'Dominica', 'Dominican Republic', 'Ecuador', 'Egypt', 'El Salvador', 'Equatorial Guinea', 'Eritrea', 'Estonia', 'Eswatini (fmr. ""Swaziland"")', 'Ethiopia', 'Fiji', 'Finland', 'France', 'Gabon', 'Gambia', 'Georgia', 'Germany', 'Ghana', 'Greece', 'Grenada', 'Guatemala', 'Guinea', 'Guinea-Bissau', 'Guyana', 'Haiti', 'Holy See', 'Honduras', 'Hungary', 'Iceland', 'India', 'Indonesia', 'Iran', 'Iraq', 'Ireland', 'Israel', 'Italy', 'Jamaica', 'Japan', 'Jordan', 'Kazakhstan', 'Kenya', 'Kiribati', 'Kuwait', 'Kyrgyzstan', 'Laos', 'Latvia', 'Lebanon', 'Lesotho', 'Liberia', 'Libya', 'Liechtenstein', 'Lithuania', 'Luxembourg', 'Madagascar', 'Malawi', 'Malaysia', 'Maldives', 'Mali', 'Malta', 'Marshall Islands', 'Mauritania', 'Mauritius', 'Mexico', 'Micronesia', 'Moldova', 'Monaco', 'Mongolia', 'Montenegro', 'Morocco', 'Mozambique', 'Myanmar (formerly Burma)', 'Namibia', 'Nauru', 'Nepal', 'Netherlands', 'New Zealand', 'Nicaragua', 'Niger', 'Nigeria', 'North Korea', 'North Macedonia', 'Norway', 'Oman', 'Pakistan', 'Palau', 'Palestine State', 'Panama', 'Papua New Guinea', 'Paraguay', 'Peru', 'Philippines', 'Poland', 'Portugal', 'Qatar', 'Romania', 'Russia', 'Rwanda', 'Saint Kitts and Nevis', 'Saint Lucia', 'Saint Vincent and the Grenadines', 'Samoa', 'San Marino', 'Sao Tome and Principe', 'Saudi Arabia', 'Senegal', 'Serbia', 'Seychelles', 'Sierra Leone', 'Singapore', 'Slovakia', 'Slovenia', 'Solomon Islands', 'Somalia', 'South Africa', 'South Korea', 'South Sudan', 'Spain', 'Sri Lanka', 'Sudan', 'Suriname', 'Sweden', 'Switzerland', 'Syria', 'Tajikistan', 'Tanzania', 'Thailand', 'Timor-Leste', 'Togo', 'Tonga', 'Trinidad and Tobago', 'Tunisia', 'Turkey', 'Turkmenistan', 'Tuvalu', 'Uganda', 'Ukraine', 'United Arab Emirates', 'United Kingdom', 'United States of America', 'Uruguay', 'Uzbekistan', 'Vanuatu', 'Venezuela', 'Vietnam', 'Yemen', 'Zambia', 'Zimbabwe',];
app.component.html
<div ngCombobox filterMode="auto-select"> <div #origin class="autocomplete"> <span class="search-icon material-symbols-outlined" translate="no">search</span> <input aria-label="Label dropdown" placeholder="Select a country" [(ngModel)]="query" ngComboboxInput /> </div> <ng-template ngComboboxPopupContainer> <ng-template [cdkConnectedOverlay]="{origin, usePopover: 'inline', matchWidth: true}" [cdkConnectedOverlayOpen]="true" > <div class="popup"> @if (countries().length === 0) { <div class="no-results">No results found</div> } <div ngListbox> @for (country of countries(); track country) { <div ngOption [value]="country" [label]="country"> <span class="option-label">{{country}}</span> <span class="check-icon material-symbols-outlined" translate="no">check</span> </div> } </div> </div> </ng-template> </ng-template></div>
app.component.css
@import url('https://fonts.googleapis.com/icon?family=Material+Symbols+Outlined');:host { display: flex; justify-content: center; font-family: var(--inter-font);}.autocomplete { display: flex; position: relative; align-items: center;}.material-symbols-outlined { font-size: 1.25rem; pointer-events: none;}.search-icon { left: 0.75rem; position: absolute; color: var(--quaternary-contrast);}[ngComboboxInput] { width: 13rem; font-size: 1rem; border-radius: 0.25rem; padding: 0.75rem 0.5rem 0.75rem 2.5rem; color: var(--primary-contrast); outline-color: var(--hot-pink); border: 1px solid var(--quinary-contrast); background-color: var(--page-background);}[ngComboboxInput]::placeholder { color: var(--quaternary-contrast);}[ngCombobox]:has([aria-expanded='false']) .popup { display: none;}.popup { width: 100%; margin-top: 8px; padding: 0.5rem; max-height: 11rem; border-radius: 0.5rem; background-color: var(--septenary-contrast); font-size: 0.9rem;}.no-results { padding: 1rem;}[ngListbox] { gap: 2px; height: 100%; display: flex; overflow: auto; flex-direction: column;}[ngOption] { display: flex; cursor: pointer; align-items: center; margin: 1px; padding: 0 1rem; min-height: 2.25rem; border-radius: 0.5rem;}[ngOption]:hover { background-color: color-mix(in srgb, var(--primary-contrast) 5%, transparent);}[ngOption][data-active='true'] { outline-offset: -2px; outline: 2px solid var(--hot-pink);}[ngOption][aria-selected='true'] { color: var(--hot-pink); background-color: color-mix(in srgb, var(--hot-pink) 5%, transparent);}[ngOption]:not([aria-selected='true']) .check-icon { display: none;}.option-label { flex: 1;}.check-icon { font-size: 0.9rem;}
app.component.ts
import {Combobox, ComboboxInput, ComboboxPopupContainer} from '@angular/aria/combobox';import {Listbox, Option} from '@angular/aria/listbox';import {OverlayModule} from '@angular/cdk/overlay';import { afterRenderEffect, ChangeDetectionStrategy, Component, computed, signal, viewChild, viewChildren,} from '@angular/core';import {FormsModule} from '@angular/forms';@Component({ selector: 'app-root', templateUrl: 'app.component.html', styleUrl: 'app.component.css', imports: [ Combobox, ComboboxInput, ComboboxPopupContainer, Listbox, Option, OverlayModule, FormsModule, ], changeDetection: ChangeDetectionStrategy.OnPush,})export class App { /** The combobox listbox popup. */ listbox = viewChild<Listbox<string>>(Listbox); /** The options available in the listbox. */ options = viewChildren<Option<string>>(Option); /** A reference to the ng aria combobox. */ combobox = viewChild<Combobox<string>>(Combobox); /** The query string used to filter the list of countries. */ query = signal(''); /** The list of countries filtered by the query. */ countries = computed(() => ALL_COUNTRIES.filter((country) => country.toLowerCase().startsWith(this.query().toLowerCase())), ); constructor() { // Scrolls to the active item when the active option changes. // The slight delay here is to ensure animations are done before scrolling. afterRenderEffect(() => { const option = this.options().find((opt) => opt.active()); setTimeout(() => option?.element.scrollIntoView({block: 'nearest'}), 50); }); // Resets the listbox scroll position when the combobox is closed. afterRenderEffect(() => { if (!this.combobox()?.expanded()) { setTimeout(() => this.listbox()?.element.scrollTo(0, 0), 150); } }); }}const ALL_COUNTRIES = [ 'Afghanistan', 'Albania', 'Algeria', 'Andorra', 'Angola', 'Antigua and Barbuda', 'Argentina', 'Armenia', 'Australia', 'Austria', 'Azerbaijan', 'Bahamas', 'Bahrain', 'Bangladesh', 'Barbados', 'Belarus', 'Belgium', 'Belize', 'Benin', 'Bhutan', 'Bolivia', 'Bosnia and Herzegovina', 'Botswana', 'Brazil', 'Brunei', 'Bulgaria', 'Burkina Faso', 'Burundi', 'Cabo Verde', 'Cambodia', 'Cameroon', 'Canada', 'Central African Republic', 'Chad', 'Chile', 'China', 'Colombia', 'Comoros', 'Congo (Congo-Brazzaville)', 'Costa Rica', "Côte d'Ivoire", 'Croatia', 'Cuba', 'Cyprus', 'Czechia (Czech Republic)', 'Democratic Republic of the Congo', 'Denmark', 'Djibouti', 'Dominica', 'Dominican Republic', 'Ecuador', 'Egypt', 'El Salvador', 'Equatorial Guinea', 'Eritrea', 'Estonia', 'Eswatini (fmr. ""Swaziland"")', 'Ethiopia', 'Fiji', 'Finland', 'France', 'Gabon', 'Gambia', 'Georgia', 'Germany', 'Ghana', 'Greece', 'Grenada', 'Guatemala', 'Guinea', 'Guinea-Bissau', 'Guyana', 'Haiti', 'Holy See', 'Honduras', 'Hungary', 'Iceland', 'India', 'Indonesia', 'Iran', 'Iraq', 'Ireland', 'Israel', 'Italy', 'Jamaica', 'Japan', 'Jordan', 'Kazakhstan', 'Kenya', 'Kiribati', 'Kuwait', 'Kyrgyzstan', 'Laos', 'Latvia', 'Lebanon', 'Lesotho', 'Liberia', 'Libya', 'Liechtenstein', 'Lithuania', 'Luxembourg', 'Madagascar', 'Malawi', 'Malaysia', 'Maldives', 'Mali', 'Malta', 'Marshall Islands', 'Mauritania', 'Mauritius', 'Mexico', 'Micronesia', 'Moldova', 'Monaco', 'Mongolia', 'Montenegro', 'Morocco', 'Mozambique', 'Myanmar (formerly Burma)', 'Namibia', 'Nauru', 'Nepal', 'Netherlands', 'New Zealand', 'Nicaragua', 'Niger', 'Nigeria', 'North Korea', 'North Macedonia', 'Norway', 'Oman', 'Pakistan', 'Palau', 'Palestine State', 'Panama', 'Papua New Guinea', 'Paraguay', 'Peru', 'Philippines', 'Poland', 'Portugal', 'Qatar', 'Romania', 'Russia', 'Rwanda', 'Saint Kitts and Nevis', 'Saint Lucia', 'Saint Vincent and the Grenadines', 'Samoa', 'San Marino', 'Sao Tome and Principe', 'Saudi Arabia', 'Senegal', 'Serbia', 'Seychelles', 'Sierra Leone', 'Singapore', 'Slovakia', 'Slovenia', 'Solomon Islands', 'Somalia', 'South Africa', 'South Korea', 'South Sudan', 'Spain', 'Sri Lanka', 'Sudan', 'Suriname', 'Sweden', 'Switzerland', 'Syria', 'Tajikistan', 'Tanzania', 'Thailand', 'Timor-Leste', 'Togo', 'Tonga', 'Trinidad and Tobago', 'Tunisia', 'Turkey', 'Turkmenistan', 'Tuvalu', 'Uganda', 'Ukraine', 'United Arab Emirates', 'United Kingdom', 'United States of America', 'Uruguay', 'Uzbekistan', 'Vanuatu', 'Venezuela', 'Vietnam', 'Yemen', 'Zambia', 'Zimbabwe',];
app.component.html
<div ngCombobox filterMode="auto-select"> <div #origin class="material-autocomplete"> <span class="search-icon material-symbols-outlined" translate="no">search</span> <input aria-label="Label dropdown" placeholder="Select a country" [(ngModel)]="query" ngComboboxInput /> </div> <ng-template ngComboboxPopupContainer> <ng-template [cdkConnectedOverlay]="{origin, usePopover: 'inline', matchWidth: true}" [cdkConnectedOverlayOpen]="true" > <div class="popup"> @if (countries().length === 0) { <div class="no-results">No results found</div> } <div ngListbox> @for (country of countries(); track country) { <div ngOption [value]="country" [label]="country"> <span class="option-label">{{country}}</span> <span class="check-icon material-symbols-outlined" translate="no">check</span> </div> } </div> </div> </ng-template> </ng-template></div>
app.component.css
@import url('https://fonts.googleapis.com/icon?family=Material+Symbols+Outlined');:host { display: flex; justify-content: center; font-family: var(--inter-font); --primary: var(--hot-pink);}.material-autocomplete { display: flex; position: relative; align-items: center;}.material-symbols-outlined { font-size: 1.25rem; pointer-events: none;}.search-icon { left: 0.75rem; position: absolute; color: var(--quaternary-contrast);}[ngComboboxInput] { width: 13rem; font-size: 1rem; border-radius: 3rem; padding: 0.75rem 0.5rem 0.75rem 2.5rem; color: var(--primary-contrast); outline-color: var(--primary); border: 1px solid var(--quinary-contrast); background-color: var(--page-background);}[ngComboboxInput]::placeholder { color: var(--quaternary-contrast);}[ngCombobox]:focus-within [ngComboboxInput] { outline: 2px solid var(--primary); outline-offset: 2px;}[ngCombobox]:has([aria-expanded='false']) .popup { display: none;}.popup { width: 100%; margin-top: 8px; padding: 0.5rem; max-height: 11rem; border-radius: 2rem; background-color: var(--septenary-contrast); font-size: 0.9rem;}.no-results { padding: 1rem;}[ngListbox] { gap: 2px; height: 100%; display: flex; overflow: auto; flex-direction: column;}[ngOption] { display: flex; cursor: pointer; align-items: center; margin: 1px; padding: 0 1rem; min-height: 3rem; border-radius: 3rem;}[ngOption]:hover,[ngOption][data-active='true'] { background-color: color-mix(in srgb, var(--primary-contrast) 5%, transparent);}[ngOption][data-active='true'] { outline-offset: -2px; outline: 2px solid var(--primary);}[ngOption][aria-selected='true'] { color: var(--primary); background-color: color-mix(in srgb, var(--primary) 10%, transparent);}[ngOption]:not([aria-selected='true']) .check-icon { display: none;}.option-label { flex: 1;}.check-icon { font-size: 0.9rem;}
app.component.ts
import {Combobox, ComboboxInput, ComboboxPopupContainer} from '@angular/aria/combobox';import {Listbox, Option} from '@angular/aria/listbox';import {OverlayModule} from '@angular/cdk/overlay';import { afterRenderEffect, ChangeDetectionStrategy, Component, computed, signal, viewChild, viewChildren,} from '@angular/core';import {FormsModule} from '@angular/forms';@Component({ selector: 'app-root', templateUrl: 'app.component.html', styleUrl: 'app.component.css', imports: [ Combobox, ComboboxInput, ComboboxPopupContainer, Listbox, Option, OverlayModule, FormsModule, ], changeDetection: ChangeDetectionStrategy.OnPush,})export class App { /** The combobox listbox popup. */ listbox = viewChild<Listbox<string>>(Listbox); /** The options available in the listbox. */ options = viewChildren<Option<string>>(Option); /** A reference to the ng aria combobox. */ combobox = viewChild<Combobox<string>>(Combobox); /** The query string used to filter the list of countries. */ query = signal(''); /** The list of countries filtered by the query. */ countries = computed(() => ALL_COUNTRIES.filter((country) => country.toLowerCase().startsWith(this.query().toLowerCase())), ); constructor() { // Scrolls to the active item when the active option changes. // The slight delay here is to ensure animations are done before scrolling. afterRenderEffect(() => { const option = this.options().find((opt) => opt.active()); setTimeout(() => option?.element.scrollIntoView({block: 'nearest'}), 50); }); // Resets the listbox scroll position when the combobox is closed. afterRenderEffect(() => { if (!this.combobox()?.expanded()) { setTimeout(() => this.listbox()?.element.scrollTo(0, 0), 150); } }); }}const ALL_COUNTRIES = [ 'Afghanistan', 'Albania', 'Algeria', 'Andorra', 'Angola', 'Antigua and Barbuda', 'Argentina', 'Armenia', 'Australia', 'Austria', 'Azerbaijan', 'Bahamas', 'Bahrain', 'Bangladesh', 'Barbados', 'Belarus', 'Belgium', 'Belize', 'Benin', 'Bhutan', 'Bolivia', 'Bosnia and Herzegovina', 'Botswana', 'Brazil', 'Brunei', 'Bulgaria', 'Burkina Faso', 'Burundi', 'Cabo Verde', 'Cambodia', 'Cameroon', 'Canada', 'Central African Republic', 'Chad', 'Chile', 'China', 'Colombia', 'Comoros', 'Congo (Congo-Brazzaville)', 'Costa Rica', "Côte d'Ivoire", 'Croatia', 'Cuba', 'Cyprus', 'Czechia (Czech Republic)', 'Democratic Republic of the Congo', 'Denmark', 'Djibouti', 'Dominica', 'Dominican Republic', 'Ecuador', 'Egypt', 'El Salvador', 'Equatorial Guinea', 'Eritrea', 'Estonia', 'Eswatini (fmr. ""Swaziland"")', 'Ethiopia', 'Fiji', 'Finland', 'France', 'Gabon', 'Gambia', 'Georgia', 'Germany', 'Ghana', 'Greece', 'Grenada', 'Guatemala', 'Guinea', 'Guinea-Bissau', 'Guyana', 'Haiti', 'Holy See', 'Honduras', 'Hungary', 'Iceland', 'India', 'Indonesia', 'Iran', 'Iraq', 'Ireland', 'Israel', 'Italy', 'Jamaica', 'Japan', 'Jordan', 'Kazakhstan', 'Kenya', 'Kiribati', 'Kuwait', 'Kyrgyzstan', 'Laos', 'Latvia', 'Lebanon', 'Lesotho', 'Liberia', 'Libya', 'Liechtenstein', 'Lithuania', 'Luxembourg', 'Madagascar', 'Malawi', 'Malaysia', 'Maldives', 'Mali', 'Malta', 'Marshall Islands', 'Mauritania', 'Mauritius', 'Mexico', 'Micronesia', 'Moldova', 'Monaco', 'Mongolia', 'Montenegro', 'Morocco', 'Mozambique', 'Myanmar (formerly Burma)', 'Namibia', 'Nauru', 'Nepal', 'Netherlands', 'New Zealand', 'Nicaragua', 'Niger', 'Nigeria', 'North Korea', 'North Macedonia', 'Norway', 'Oman', 'Pakistan', 'Palau', 'Palestine State', 'Panama', 'Papua New Guinea', 'Paraguay', 'Peru', 'Philippines', 'Poland', 'Portugal', 'Qatar', 'Romania', 'Russia', 'Rwanda', 'Saint Kitts and Nevis', 'Saint Lucia', 'Saint Vincent and the Grenadines', 'Samoa', 'San Marino', 'Sao Tome and Principe', 'Saudi Arabia', 'Senegal', 'Serbia', 'Seychelles', 'Sierra Leone', 'Singapore', 'Slovakia', 'Slovenia', 'Solomon Islands', 'Somalia', 'South Africa', 'South Korea', 'South Sudan', 'Spain', 'Sri Lanka', 'Sudan', 'Suriname', 'Sweden', 'Switzerland', 'Syria', 'Tajikistan', 'Tanzania', 'Thailand', 'Timor-Leste', 'Togo', 'Tonga', 'Trinidad and Tobago', 'Tunisia', 'Turkey', 'Turkmenistan', 'Tuvalu', 'Uganda', 'Ukraine', 'United Arab Emirates', 'United Kingdom', 'United States of America', 'Uruguay', 'Uzbekistan', 'Vanuatu', 'Venezuela', 'Vietnam', 'Yemen', 'Zambia', 'Zimbabwe',];
app.component.html
<div ngCombobox filterMode="auto-select"> <div #origin class="retro-autocomplete"> <span class="search-icon material-symbols-outlined" translate="no">search</span> <input aria-label="Label dropdown" placeholder="Select a country" [(ngModel)]="query" ngComboboxInput /> </div> <ng-template ngComboboxPopupContainer> <ng-template [cdkConnectedOverlay]="{origin, usePopover: 'inline', matchWidth: true}" [cdkConnectedOverlayOpen]="true" > <div class="popup"> @if (countries().length === 0) { <div class="no-results">No results found</div> } <div ngListbox> @for (country of countries(); track country) { <div ngOption [value]="country" [label]="country"> <span class="option-label">{{country}}</span> <span class="check-icon material-symbols-outlined" translate="no">check</span> </div> } </div> </div> </ng-template> </ng-template></div>
app.component.css
@import url('https://fonts.googleapis.com/icon?family=Material+Symbols+Outlined');@import url('https://fonts.googleapis.com/css2?family=Press+Start+2P&display=swap');:host { display: flex; justify-content: center; font-size: 0.6rem; font-family: 'Press Start 2P'; --retro-button-color: #fff; --retro-shadow-light: color-mix(in srgb, var(--retro-button-color) 90%, #fff); --retro-shadow-dark: color-mix(in srgb, var(--retro-button-color) 90%, #000); --retro-elevated-shadow: inset 4px 4px 0px 0px var(--retro-shadow-light), inset -4px -4px 0px 0px var(--retro-shadow-dark), 4px 0px 0px 0px var(--gray-700), 0px 4px 0px 0px var(--gray-700), -4px 0px 0px 0px var(--gray-700), 0px -4px 0px 0px var(--gray-700); --retro-flat-shadow: 4px 0px 0px 0px var(--gray-700), 0px 4px 0px 0px var(--gray-700), -4px 0px 0px 0px var(--gray-700), 0px -4px 0px 0px var(--gray-700); --retro-pressed-shadow: inset 4px 4px 0px 0px var(--retro-shadow-dark), inset -4px -4px 0px 0px var(--retro-shadow-light), 4px 0px 0px 0px var(--gray-700), 0px 4px 0px 0px var(--gray-700), -4px 0px 0px 0px var(--gray-700), 0px -4px 0px 0px var(--gray-700), 0px 0px 0px 0px var(--gray-700);}.retro-autocomplete { display: flex; position: relative; align-items: center;}.material-symbols-outlined { font-size: 1.25rem; pointer-events: none;}.search-icon { left: 0.75rem; position: absolute; color: #000; z-index: 1;}[ngComboboxInput] { width: 15rem; font-size: 0.6rem; border-radius: 0; font-family: 'Press Start 2P'; word-spacing: -5px; padding: 0.75rem 0.5rem 0.75rem 2.5rem; color: #000; border: none; box-shadow: var(--retro-flat-shadow); background-color: var(--retro-button-color);}[ngComboboxInput]::placeholder { color: #000; opacity: 0.7;}[ngComboboxInput]:focus { outline: none; transform: translate(1px, 1px); box-shadow: var(--retro-pressed-shadow);}[ngCombobox]:has([aria-expanded='false']) .popup { display: none;}.popup { width: 100%; margin-top: 20px; padding: 0.5rem; max-height: 11rem; border-radius: 0; background-color: var(--septenary-contrast); box-shadow: var(--retro-flat-shadow);}.no-results { padding: 1rem;}[ngListbox] { gap: 2px; height: 100%; display: flex; overflow: auto; flex-direction: column;}[ngOption] { display: flex; cursor: pointer; align-items: center; margin: 1px; padding: 0 1rem; min-height: 2.25rem; border-radius: 0;}[ngOption]:hover { background-color: color-mix(in srgb, var(--primary-contrast) 5%, transparent);}[ngOption][data-active='true'] { outline-offset: -2px; outline: 2px dashed var(--hot-pink);}[ngOption][aria-selected='true'] { color: var(--hot-pink); background-color: color-mix(in srgb, var(--hot-pink) 5%, transparent);}[ngOption]:not([aria-selected='true']) .check-icon { display: none;}.option-label { flex: 1;}.check-icon { font-size: 0.9rem;}
使い方
コンボボックスは、テキスト入力とポップアップを連携させるプリミティブディレクティブです。オートコンプリート、セレクト、マルチセレクトパターンの基盤を提供します。次のような場合には、コンボボックスを直接使用することを検討してください:
- カスタムオートコンプリートパターンの構築 - 特殊なフィルタリングやサジェスチョンの動作を作成する
- カスタム選択コンポーネントの作成 - 独自の要件を持つドロップダウンを開発する
- 入力とポップアップの連携 - テキスト入力をリストボックス、ツリー、またはダイアログコンテンツと組み合わせる
- 特定のフィルターモードの実装 - 手動、自動選択、またはハイライトの動作を使用する
代わりに、次のような場合はドキュメント化されたパターンを使用してください:
- フィルタリング付きの標準的なオートコンプリートが必要な場合 - すぐに使える例については、Autocompleteパターンを参照してください
- 単一選択のドロップダウンが必要な場合 - 完全なドロップダウンの実装については、Selectパターンを参照してください
- 複数選択のドロップダウンが必要な場合 - コンパクトな表示の複数選択については、Multiselectパターンを参照してください
Note: Autocomplete、Select、Multiselectのガイドでは、このディレクティブを特定のユースケースのためにListboxと組み合わせた、ドキュメント化されたパターンが示されています。
機能
Angularのコンボボックスは、完全にアクセシブルな入力とポップアップの連携システムを以下の機能とともに提供します:
- ポップアップ付きテキスト入力 - 入力フィールドとポップアップコンテンツを連携させます
- 3つのフィルターモード - 手動、自動選択、またはハイライトの動作
- キーボードナビゲーション - 矢印キー、Enter、Escapeキーのハンドリング
- スクリーンリーダーのサポート -
role="combobox"やaria-expandedを含む組み込みのARIA属性 - ポップアップ管理 - ユーザーインタラクションに基づく自動的な表示/非表示
- シグナルベースのリアクティビティ - Angularシグナルを使用したリアクティブな状態管理
例
オートコンプリート
ユーザーが入力するにつれてオプションをフィルタリングして提案する、アクセシブルな入力フィールドです。リストから値を見つけて選択するのに役立ちます。
app.component.ts
import {Combobox, ComboboxInput, ComboboxPopupContainer} from '@angular/aria/combobox';import {Listbox, Option} from '@angular/aria/listbox';import {OverlayModule} from '@angular/cdk/overlay';import { afterRenderEffect, ChangeDetectionStrategy, Component, computed, signal, viewChild, viewChildren,} from '@angular/core';import {FormsModule} from '@angular/forms';@Component({ selector: 'app-root', templateUrl: 'app.component.html', styleUrl: 'app.component.css', imports: [ Combobox, ComboboxInput, ComboboxPopupContainer, Listbox, Option, OverlayModule, FormsModule, ], changeDetection: ChangeDetectionStrategy.OnPush,})export class App { /** The combobox listbox popup. */ listbox = viewChild<Listbox<string>>(Listbox); /** The options available in the listbox. */ options = viewChildren<Option<string>>(Option); /** A reference to the ng aria combobox. */ combobox = viewChild<Combobox<string>>(Combobox); /** The query string used to filter the list of countries. */ query = signal(''); /** The list of countries filtered by the query. */ countries = computed(() => ALL_COUNTRIES.filter((country) => country.toLowerCase().startsWith(this.query().toLowerCase())), ); constructor() { // Scrolls to the active item when the active option changes. // The slight delay here is to ensure animations are done before scrolling. afterRenderEffect(() => { const option = this.options().find((opt) => opt.active()); setTimeout(() => option?.element.scrollIntoView({block: 'nearest'}), 50); }); // Resets the listbox scroll position when the combobox is closed. afterRenderEffect(() => { if (!this.combobox()?.expanded()) { setTimeout(() => this.listbox()?.element.scrollTo(0, 0), 150); } }); }}const ALL_COUNTRIES = [ 'Afghanistan', 'Albania', 'Algeria', 'Andorra', 'Angola', 'Antigua and Barbuda', 'Argentina', 'Armenia', 'Australia', 'Austria', 'Azerbaijan', 'Bahamas', 'Bahrain', 'Bangladesh', 'Barbados', 'Belarus', 'Belgium', 'Belize', 'Benin', 'Bhutan', 'Bolivia', 'Bosnia and Herzegovina', 'Botswana', 'Brazil', 'Brunei', 'Bulgaria', 'Burkina Faso', 'Burundi', 'Cabo Verde', 'Cambodia', 'Cameroon', 'Canada', 'Central African Republic', 'Chad', 'Chile', 'China', 'Colombia', 'Comoros', 'Congo (Congo-Brazzaville)', 'Costa Rica', "Côte d'Ivoire", 'Croatia', 'Cuba', 'Cyprus', 'Czechia (Czech Republic)', 'Democratic Republic of the Congo', 'Denmark', 'Djibouti', 'Dominica', 'Dominican Republic', 'Ecuador', 'Egypt', 'El Salvador', 'Equatorial Guinea', 'Eritrea', 'Estonia', 'Eswatini (fmr. ""Swaziland"")', 'Ethiopia', 'Fiji', 'Finland', 'France', 'Gabon', 'Gambia', 'Georgia', 'Germany', 'Ghana', 'Greece', 'Grenada', 'Guatemala', 'Guinea', 'Guinea-Bissau', 'Guyana', 'Haiti', 'Holy See', 'Honduras', 'Hungary', 'Iceland', 'India', 'Indonesia', 'Iran', 'Iraq', 'Ireland', 'Israel', 'Italy', 'Jamaica', 'Japan', 'Jordan', 'Kazakhstan', 'Kenya', 'Kiribati', 'Kuwait', 'Kyrgyzstan', 'Laos', 'Latvia', 'Lebanon', 'Lesotho', 'Liberia', 'Libya', 'Liechtenstein', 'Lithuania', 'Luxembourg', 'Madagascar', 'Malawi', 'Malaysia', 'Maldives', 'Mali', 'Malta', 'Marshall Islands', 'Mauritania', 'Mauritius', 'Mexico', 'Micronesia', 'Moldova', 'Monaco', 'Mongolia', 'Montenegro', 'Morocco', 'Mozambique', 'Myanmar (formerly Burma)', 'Namibia', 'Nauru', 'Nepal', 'Netherlands', 'New Zealand', 'Nicaragua', 'Niger', 'Nigeria', 'North Korea', 'North Macedonia', 'Norway', 'Oman', 'Pakistan', 'Palau', 'Palestine State', 'Panama', 'Papua New Guinea', 'Paraguay', 'Peru', 'Philippines', 'Poland', 'Portugal', 'Qatar', 'Romania', 'Russia', 'Rwanda', 'Saint Kitts and Nevis', 'Saint Lucia', 'Saint Vincent and the Grenadines', 'Samoa', 'San Marino', 'Sao Tome and Principe', 'Saudi Arabia', 'Senegal', 'Serbia', 'Seychelles', 'Sierra Leone', 'Singapore', 'Slovakia', 'Slovenia', 'Solomon Islands', 'Somalia', 'South Africa', 'South Korea', 'South Sudan', 'Spain', 'Sri Lanka', 'Sudan', 'Suriname', 'Sweden', 'Switzerland', 'Syria', 'Tajikistan', 'Tanzania', 'Thailand', 'Timor-Leste', 'Togo', 'Tonga', 'Trinidad and Tobago', 'Tunisia', 'Turkey', 'Turkmenistan', 'Tuvalu', 'Uganda', 'Ukraine', 'United Arab Emirates', 'United Kingdom', 'United States of America', 'Uruguay', 'Uzbekistan', 'Vanuatu', 'Venezuela', 'Vietnam', 'Yemen', 'Zambia', 'Zimbabwe',];
app.component.html
<div ngCombobox filterMode="auto-select"> <div #origin class="autocomplete"> <span class="search-icon material-symbols-outlined" translate="no">search</span> <input aria-label="Label dropdown" placeholder="Select a country" [(ngModel)]="query" ngComboboxInput /> </div> <ng-template ngComboboxPopupContainer> <ng-template [cdkConnectedOverlay]="{origin, usePopover: 'inline', matchWidth: true}" [cdkConnectedOverlayOpen]="true" > <div class="popup"> @if (countries().length === 0) { <div class="no-results">No results found</div> } <div ngListbox> @for (country of countries(); track country) { <div ngOption [value]="country" [label]="country"> <span class="option-label">{{country}}</span> <span class="check-icon material-symbols-outlined" translate="no">check</span> </div> } </div> </div> </ng-template> </ng-template></div>
app.component.css
@import url('https://fonts.googleapis.com/icon?family=Material+Symbols+Outlined');:host { display: flex; justify-content: center; font-family: var(--inter-font);}.autocomplete { display: flex; position: relative; align-items: center;}.material-symbols-outlined { font-size: 1.25rem; pointer-events: none;}.search-icon { left: 0.75rem; position: absolute; color: var(--quaternary-contrast);}[ngComboboxInput] { width: 13rem; font-size: 1rem; border-radius: 0.25rem; padding: 0.75rem 0.5rem 0.75rem 2.5rem; color: var(--primary-contrast); outline-color: var(--hot-pink); border: 1px solid var(--quinary-contrast); background-color: var(--page-background);}[ngComboboxInput]::placeholder { color: var(--quaternary-contrast);}[ngCombobox]:has([aria-expanded='false']) .popup { display: none;}.popup { width: 100%; margin-top: 8px; padding: 0.5rem; max-height: 11rem; border-radius: 0.5rem; background-color: var(--septenary-contrast); font-size: 0.9rem;}.no-results { padding: 1rem;}[ngListbox] { gap: 2px; height: 100%; display: flex; overflow: auto; flex-direction: column;}[ngOption] { display: flex; cursor: pointer; align-items: center; margin: 1px; padding: 0 1rem; min-height: 2.25rem; border-radius: 0.5rem;}[ngOption]:hover { background-color: color-mix(in srgb, var(--primary-contrast) 5%, transparent);}[ngOption][data-active='true'] { outline-offset: -2px; outline: 2px solid var(--hot-pink);}[ngOption][aria-selected='true'] { color: var(--hot-pink); background-color: color-mix(in srgb, var(--hot-pink) 5%, transparent);}[ngOption]:not([aria-selected='true']) .check-icon { display: none;}.option-label { flex: 1;}.check-icon { font-size: 0.9rem;}
app.component.ts
import {Combobox, ComboboxInput, ComboboxPopupContainer} from '@angular/aria/combobox';import {Listbox, Option} from '@angular/aria/listbox';import {OverlayModule} from '@angular/cdk/overlay';import { afterRenderEffect, ChangeDetectionStrategy, Component, computed, signal, viewChild, viewChildren,} from '@angular/core';import {FormsModule} from '@angular/forms';@Component({ selector: 'app-root', templateUrl: 'app.component.html', styleUrl: 'app.component.css', imports: [ Combobox, ComboboxInput, ComboboxPopupContainer, Listbox, Option, OverlayModule, FormsModule, ], changeDetection: ChangeDetectionStrategy.OnPush,})export class App { /** The combobox listbox popup. */ listbox = viewChild<Listbox<string>>(Listbox); /** The options available in the listbox. */ options = viewChildren<Option<string>>(Option); /** A reference to the ng aria combobox. */ combobox = viewChild<Combobox<string>>(Combobox); /** The query string used to filter the list of countries. */ query = signal(''); /** The list of countries filtered by the query. */ countries = computed(() => ALL_COUNTRIES.filter((country) => country.toLowerCase().startsWith(this.query().toLowerCase())), ); constructor() { // Scrolls to the active item when the active option changes. // The slight delay here is to ensure animations are done before scrolling. afterRenderEffect(() => { const option = this.options().find((opt) => opt.active()); setTimeout(() => option?.element.scrollIntoView({block: 'nearest'}), 50); }); // Resets the listbox scroll position when the combobox is closed. afterRenderEffect(() => { if (!this.combobox()?.expanded()) { setTimeout(() => this.listbox()?.element.scrollTo(0, 0), 150); } }); }}const ALL_COUNTRIES = [ 'Afghanistan', 'Albania', 'Algeria', 'Andorra', 'Angola', 'Antigua and Barbuda', 'Argentina', 'Armenia', 'Australia', 'Austria', 'Azerbaijan', 'Bahamas', 'Bahrain', 'Bangladesh', 'Barbados', 'Belarus', 'Belgium', 'Belize', 'Benin', 'Bhutan', 'Bolivia', 'Bosnia and Herzegovina', 'Botswana', 'Brazil', 'Brunei', 'Bulgaria', 'Burkina Faso', 'Burundi', 'Cabo Verde', 'Cambodia', 'Cameroon', 'Canada', 'Central African Republic', 'Chad', 'Chile', 'China', 'Colombia', 'Comoros', 'Congo (Congo-Brazzaville)', 'Costa Rica', "Côte d'Ivoire", 'Croatia', 'Cuba', 'Cyprus', 'Czechia (Czech Republic)', 'Democratic Republic of the Congo', 'Denmark', 'Djibouti', 'Dominica', 'Dominican Republic', 'Ecuador', 'Egypt', 'El Salvador', 'Equatorial Guinea', 'Eritrea', 'Estonia', 'Eswatini (fmr. ""Swaziland"")', 'Ethiopia', 'Fiji', 'Finland', 'France', 'Gabon', 'Gambia', 'Georgia', 'Germany', 'Ghana', 'Greece', 'Grenada', 'Guatemala', 'Guinea', 'Guinea-Bissau', 'Guyana', 'Haiti', 'Holy See', 'Honduras', 'Hungary', 'Iceland', 'India', 'Indonesia', 'Iran', 'Iraq', 'Ireland', 'Israel', 'Italy', 'Jamaica', 'Japan', 'Jordan', 'Kazakhstan', 'Kenya', 'Kiribati', 'Kuwait', 'Kyrgyzstan', 'Laos', 'Latvia', 'Lebanon', 'Lesotho', 'Liberia', 'Libya', 'Liechtenstein', 'Lithuania', 'Luxembourg', 'Madagascar', 'Malawi', 'Malaysia', 'Maldives', 'Mali', 'Malta', 'Marshall Islands', 'Mauritania', 'Mauritius', 'Mexico', 'Micronesia', 'Moldova', 'Monaco', 'Mongolia', 'Montenegro', 'Morocco', 'Mozambique', 'Myanmar (formerly Burma)', 'Namibia', 'Nauru', 'Nepal', 'Netherlands', 'New Zealand', 'Nicaragua', 'Niger', 'Nigeria', 'North Korea', 'North Macedonia', 'Norway', 'Oman', 'Pakistan', 'Palau', 'Palestine State', 'Panama', 'Papua New Guinea', 'Paraguay', 'Peru', 'Philippines', 'Poland', 'Portugal', 'Qatar', 'Romania', 'Russia', 'Rwanda', 'Saint Kitts and Nevis', 'Saint Lucia', 'Saint Vincent and the Grenadines', 'Samoa', 'San Marino', 'Sao Tome and Principe', 'Saudi Arabia', 'Senegal', 'Serbia', 'Seychelles', 'Sierra Leone', 'Singapore', 'Slovakia', 'Slovenia', 'Solomon Islands', 'Somalia', 'South Africa', 'South Korea', 'South Sudan', 'Spain', 'Sri Lanka', 'Sudan', 'Suriname', 'Sweden', 'Switzerland', 'Syria', 'Tajikistan', 'Tanzania', 'Thailand', 'Timor-Leste', 'Togo', 'Tonga', 'Trinidad and Tobago', 'Tunisia', 'Turkey', 'Turkmenistan', 'Tuvalu', 'Uganda', 'Ukraine', 'United Arab Emirates', 'United Kingdom', 'United States of America', 'Uruguay', 'Uzbekistan', 'Vanuatu', 'Venezuela', 'Vietnam', 'Yemen', 'Zambia', 'Zimbabwe',];
app.component.html
<div ngCombobox filterMode="auto-select"> <div #origin class="material-autocomplete"> <span class="search-icon material-symbols-outlined" translate="no">search</span> <input aria-label="Label dropdown" placeholder="Select a country" [(ngModel)]="query" ngComboboxInput /> </div> <ng-template ngComboboxPopupContainer> <ng-template [cdkConnectedOverlay]="{origin, usePopover: 'inline', matchWidth: true}" [cdkConnectedOverlayOpen]="true" > <div class="popup"> @if (countries().length === 0) { <div class="no-results">No results found</div> } <div ngListbox> @for (country of countries(); track country) { <div ngOption [value]="country" [label]="country"> <span class="option-label">{{country}}</span> <span class="check-icon material-symbols-outlined" translate="no">check</span> </div> } </div> </div> </ng-template> </ng-template></div>
app.component.css
@import url('https://fonts.googleapis.com/icon?family=Material+Symbols+Outlined');:host { display: flex; justify-content: center; font-family: var(--inter-font); --primary: var(--hot-pink);}.material-autocomplete { display: flex; position: relative; align-items: center;}.material-symbols-outlined { font-size: 1.25rem; pointer-events: none;}.search-icon { left: 0.75rem; position: absolute; color: var(--quaternary-contrast);}[ngComboboxInput] { width: 13rem; font-size: 1rem; border-radius: 3rem; padding: 0.75rem 0.5rem 0.75rem 2.5rem; color: var(--primary-contrast); outline-color: var(--primary); border: 1px solid var(--quinary-contrast); background-color: var(--page-background);}[ngComboboxInput]::placeholder { color: var(--quaternary-contrast);}[ngCombobox]:focus-within [ngComboboxInput] { outline: 2px solid var(--primary); outline-offset: 2px;}[ngCombobox]:has([aria-expanded='false']) .popup { display: none;}.popup { width: 100%; margin-top: 8px; padding: 0.5rem; max-height: 11rem; border-radius: 2rem; background-color: var(--septenary-contrast); font-size: 0.9rem;}.no-results { padding: 1rem;}[ngListbox] { gap: 2px; height: 100%; display: flex; overflow: auto; flex-direction: column;}[ngOption] { display: flex; cursor: pointer; align-items: center; margin: 1px; padding: 0 1rem; min-height: 3rem; border-radius: 3rem;}[ngOption]:hover,[ngOption][data-active='true'] { background-color: color-mix(in srgb, var(--primary-contrast) 5%, transparent);}[ngOption][data-active='true'] { outline-offset: -2px; outline: 2px solid var(--primary);}[ngOption][aria-selected='true'] { color: var(--primary); background-color: color-mix(in srgb, var(--primary) 10%, transparent);}[ngOption]:not([aria-selected='true']) .check-icon { display: none;}.option-label { flex: 1;}.check-icon { font-size: 0.9rem;}
app.component.ts
import {Combobox, ComboboxInput, ComboboxPopupContainer} from '@angular/aria/combobox';import {Listbox, Option} from '@angular/aria/listbox';import {OverlayModule} from '@angular/cdk/overlay';import { afterRenderEffect, ChangeDetectionStrategy, Component, computed, signal, viewChild, viewChildren,} from '@angular/core';import {FormsModule} from '@angular/forms';@Component({ selector: 'app-root', templateUrl: 'app.component.html', styleUrl: 'app.component.css', imports: [ Combobox, ComboboxInput, ComboboxPopupContainer, Listbox, Option, OverlayModule, FormsModule, ], changeDetection: ChangeDetectionStrategy.OnPush,})export class App { /** The combobox listbox popup. */ listbox = viewChild<Listbox<string>>(Listbox); /** The options available in the listbox. */ options = viewChildren<Option<string>>(Option); /** A reference to the ng aria combobox. */ combobox = viewChild<Combobox<string>>(Combobox); /** The query string used to filter the list of countries. */ query = signal(''); /** The list of countries filtered by the query. */ countries = computed(() => ALL_COUNTRIES.filter((country) => country.toLowerCase().startsWith(this.query().toLowerCase())), ); constructor() { // Scrolls to the active item when the active option changes. // The slight delay here is to ensure animations are done before scrolling. afterRenderEffect(() => { const option = this.options().find((opt) => opt.active()); setTimeout(() => option?.element.scrollIntoView({block: 'nearest'}), 50); }); // Resets the listbox scroll position when the combobox is closed. afterRenderEffect(() => { if (!this.combobox()?.expanded()) { setTimeout(() => this.listbox()?.element.scrollTo(0, 0), 150); } }); }}const ALL_COUNTRIES = [ 'Afghanistan', 'Albania', 'Algeria', 'Andorra', 'Angola', 'Antigua and Barbuda', 'Argentina', 'Armenia', 'Australia', 'Austria', 'Azerbaijan', 'Bahamas', 'Bahrain', 'Bangladesh', 'Barbados', 'Belarus', 'Belgium', 'Belize', 'Benin', 'Bhutan', 'Bolivia', 'Bosnia and Herzegovina', 'Botswana', 'Brazil', 'Brunei', 'Bulgaria', 'Burkina Faso', 'Burundi', 'Cabo Verde', 'Cambodia', 'Cameroon', 'Canada', 'Central African Republic', 'Chad', 'Chile', 'China', 'Colombia', 'Comoros', 'Congo (Congo-Brazzaville)', 'Costa Rica', "Côte d'Ivoire", 'Croatia', 'Cuba', 'Cyprus', 'Czechia (Czech Republic)', 'Democratic Republic of the Congo', 'Denmark', 'Djibouti', 'Dominica', 'Dominican Republic', 'Ecuador', 'Egypt', 'El Salvador', 'Equatorial Guinea', 'Eritrea', 'Estonia', 'Eswatini (fmr. ""Swaziland"")', 'Ethiopia', 'Fiji', 'Finland', 'France', 'Gabon', 'Gambia', 'Georgia', 'Germany', 'Ghana', 'Greece', 'Grenada', 'Guatemala', 'Guinea', 'Guinea-Bissau', 'Guyana', 'Haiti', 'Holy See', 'Honduras', 'Hungary', 'Iceland', 'India', 'Indonesia', 'Iran', 'Iraq', 'Ireland', 'Israel', 'Italy', 'Jamaica', 'Japan', 'Jordan', 'Kazakhstan', 'Kenya', 'Kiribati', 'Kuwait', 'Kyrgyzstan', 'Laos', 'Latvia', 'Lebanon', 'Lesotho', 'Liberia', 'Libya', 'Liechtenstein', 'Lithuania', 'Luxembourg', 'Madagascar', 'Malawi', 'Malaysia', 'Maldives', 'Mali', 'Malta', 'Marshall Islands', 'Mauritania', 'Mauritius', 'Mexico', 'Micronesia', 'Moldova', 'Monaco', 'Mongolia', 'Montenegro', 'Morocco', 'Mozambique', 'Myanmar (formerly Burma)', 'Namibia', 'Nauru', 'Nepal', 'Netherlands', 'New Zealand', 'Nicaragua', 'Niger', 'Nigeria', 'North Korea', 'North Macedonia', 'Norway', 'Oman', 'Pakistan', 'Palau', 'Palestine State', 'Panama', 'Papua New Guinea', 'Paraguay', 'Peru', 'Philippines', 'Poland', 'Portugal', 'Qatar', 'Romania', 'Russia', 'Rwanda', 'Saint Kitts and Nevis', 'Saint Lucia', 'Saint Vincent and the Grenadines', 'Samoa', 'San Marino', 'Sao Tome and Principe', 'Saudi Arabia', 'Senegal', 'Serbia', 'Seychelles', 'Sierra Leone', 'Singapore', 'Slovakia', 'Slovenia', 'Solomon Islands', 'Somalia', 'South Africa', 'South Korea', 'South Sudan', 'Spain', 'Sri Lanka', 'Sudan', 'Suriname', 'Sweden', 'Switzerland', 'Syria', 'Tajikistan', 'Tanzania', 'Thailand', 'Timor-Leste', 'Togo', 'Tonga', 'Trinidad and Tobago', 'Tunisia', 'Turkey', 'Turkmenistan', 'Tuvalu', 'Uganda', 'Ukraine', 'United Arab Emirates', 'United Kingdom', 'United States of America', 'Uruguay', 'Uzbekistan', 'Vanuatu', 'Venezuela', 'Vietnam', 'Yemen', 'Zambia', 'Zimbabwe',];
app.component.html
<div ngCombobox filterMode="auto-select"> <div #origin class="retro-autocomplete"> <span class="search-icon material-symbols-outlined" translate="no">search</span> <input aria-label="Label dropdown" placeholder="Select a country" [(ngModel)]="query" ngComboboxInput /> </div> <ng-template ngComboboxPopupContainer> <ng-template [cdkConnectedOverlay]="{origin, usePopover: 'inline', matchWidth: true}" [cdkConnectedOverlayOpen]="true" > <div class="popup"> @if (countries().length === 0) { <div class="no-results">No results found</div> } <div ngListbox> @for (country of countries(); track country) { <div ngOption [value]="country" [label]="country"> <span class="option-label">{{country}}</span> <span class="check-icon material-symbols-outlined" translate="no">check</span> </div> } </div> </div> </ng-template> </ng-template></div>
app.component.css
@import url('https://fonts.googleapis.com/icon?family=Material+Symbols+Outlined');@import url('https://fonts.googleapis.com/css2?family=Press+Start+2P&display=swap');:host { display: flex; justify-content: center; font-size: 0.6rem; font-family: 'Press Start 2P'; --retro-button-color: #fff; --retro-shadow-light: color-mix(in srgb, var(--retro-button-color) 90%, #fff); --retro-shadow-dark: color-mix(in srgb, var(--retro-button-color) 90%, #000); --retro-elevated-shadow: inset 4px 4px 0px 0px var(--retro-shadow-light), inset -4px -4px 0px 0px var(--retro-shadow-dark), 4px 0px 0px 0px var(--gray-700), 0px 4px 0px 0px var(--gray-700), -4px 0px 0px 0px var(--gray-700), 0px -4px 0px 0px var(--gray-700); --retro-flat-shadow: 4px 0px 0px 0px var(--gray-700), 0px 4px 0px 0px var(--gray-700), -4px 0px 0px 0px var(--gray-700), 0px -4px 0px 0px var(--gray-700); --retro-pressed-shadow: inset 4px 4px 0px 0px var(--retro-shadow-dark), inset -4px -4px 0px 0px var(--retro-shadow-light), 4px 0px 0px 0px var(--gray-700), 0px 4px 0px 0px var(--gray-700), -4px 0px 0px 0px var(--gray-700), 0px -4px 0px 0px var(--gray-700), 0px 0px 0px 0px var(--gray-700);}.retro-autocomplete { display: flex; position: relative; align-items: center;}.material-symbols-outlined { font-size: 1.25rem; pointer-events: none;}.search-icon { left: 0.75rem; position: absolute; color: #000; z-index: 1;}[ngComboboxInput] { width: 15rem; font-size: 0.6rem; border-radius: 0; font-family: 'Press Start 2P'; word-spacing: -5px; padding: 0.75rem 0.5rem 0.75rem 2.5rem; color: #000; border: none; box-shadow: var(--retro-flat-shadow); background-color: var(--retro-button-color);}[ngComboboxInput]::placeholder { color: #000; opacity: 0.7;}[ngComboboxInput]:focus { outline: none; transform: translate(1px, 1px); box-shadow: var(--retro-pressed-shadow);}[ngCombobox]:has([aria-expanded='false']) .popup { display: none;}.popup { width: 100%; margin-top: 20px; padding: 0.5rem; max-height: 11rem; border-radius: 0; background-color: var(--septenary-contrast); box-shadow: var(--retro-flat-shadow);}.no-results { padding: 1rem;}[ngListbox] { gap: 2px; height: 100%; display: flex; overflow: auto; flex-direction: column;}[ngOption] { display: flex; cursor: pointer; align-items: center; margin: 1px; padding: 0 1rem; min-height: 2.25rem; border-radius: 0;}[ngOption]:hover { background-color: color-mix(in srgb, var(--primary-contrast) 5%, transparent);}[ngOption][data-active='true'] { outline-offset: -2px; outline: 2px dashed var(--hot-pink);}[ngOption][aria-selected='true'] { color: var(--hot-pink); background-color: color-mix(in srgb, var(--hot-pink) 5%, transparent);}[ngOption]:not([aria-selected='true']) .check-icon { display: none;}.option-label { flex: 1;}.check-icon { font-size: 0.9rem;}
filterMode="manual"設定は、フィルタリングと選択を完全に制御します。入力は、オプションリストをフィルタリングするシグナルを更新します。ユーザーは矢印キーで移動し、Enterキーまたはクリックで選択します。このモードは、カスタムフィルタリングロジックに最も柔軟性を提供します。完全なフィルタリングパターンと例については、オートコンプリートガイドを参照してください。
読み取り専用モード
読み取り専用のコンボボックスとリストボックスを組み合わせて、キーボードナビゲーションとスクリーンリーダーをサポートする単一選択のドロップダウンを作成するパターンです。
app.ts
import { Combobox, ComboboxInput, ComboboxPopup, ComboboxPopupContainer,} from '@angular/aria/combobox';import {Listbox, Option} from '@angular/aria/listbox';import { afterRenderEffect, ChangeDetectionStrategy, Component, computed, signal, viewChild, viewChildren,} from '@angular/core';import {OverlayModule} from '@angular/cdk/overlay';@Component({ selector: 'app-root', templateUrl: './app.html', styleUrl: './app.css', imports: [ Combobox, ComboboxInput, ComboboxPopup, ComboboxPopupContainer, Listbox, Option, OverlayModule, ], changeDetection: ChangeDetectionStrategy.OnPush,})export class App { /** The combobox listbox popup. */ listbox = viewChild<Listbox<string>>(Listbox); /** The options available in the listbox. */ options = viewChildren<Option<string>>(Option); /** A reference to the ng aria combobox. */ combobox = viewChild<Combobox<string>>(Combobox); /** The icon that is displayed in the combobox. */ displayIcon = computed(() => { const values = this.listbox()?.values() || []; const label = this.labels.find((label) => label.value === values[0]); return label ? label.icon : ''; }); /** The string that is displayed in the combobox. */ displayValue = computed(() => { const values = this.listbox()?.values() || []; return values.length ? values[0] : 'Select a label'; }); /** The labels that are available for selection. */ labels = [ {value: 'Important', icon: 'label'}, {value: 'Starred', icon: 'star'}, {value: 'Work', icon: 'work'}, {value: 'Personal', icon: 'person'}, {value: 'To Do', icon: 'checklist'}, {value: 'Later', icon: 'schedule'}, {value: 'Read', icon: 'menu_book'}, {value: 'Travel', icon: 'flight'}, ]; constructor() { // Scrolls to the active item when the active option changes. // The slight delay here is to ensure animations are done before scrolling. afterRenderEffect(() => { const option = this.options().find((opt) => opt.active()); setTimeout(() => option?.element.scrollIntoView({block: 'nearest'}), 50); }); // Resets the listbox scroll position when the combobox is closed. afterRenderEffect(() => { if (!this.combobox()?.expanded()) { setTimeout(() => this.listbox()?.element.scrollTo(0, 0), 150); } }); }}
app.html
<div ngCombobox readonly> <div #origin class="select"> <span class="combobox-label"> <span class="selected-label-icon material-symbols-outlined" translate="no">{{ displayIcon() }}</span> <span class="selected-label-text">{{ displayValue() }}</span> </span> <input aria-label="Label dropdown" placeholder="Select a label" ngComboboxInput /> <span class="example-arrow material-symbols-outlined" translate="no">arrow_drop_down</span> </div> <ng-template ngComboboxPopupContainer> <ng-template [cdkConnectedOverlay]="{origin, usePopover: 'inline', matchWidth: true}" [cdkConnectedOverlayOpen]="true" > <div class="example-popup-container"> <div ngListbox> @for (label of labels; track label.value) { <div ngOption [value]="label.value" [label]="label.value"> <span class="example-option-icon material-symbols-outlined" translate="no">{{label.icon}}</span> <span class="example-option-text">{{label.value}}</span> <span class="example-option-check material-symbols-outlined" translate="no">check</span> </div> } </div> </div> </ng-template> </ng-template></div>
app.css
@import url('https://fonts.googleapis.com/icon?family=Material+Symbols+Outlined');:host { display: flex; justify-content: center; font-family: var(--inter-font);}.select { display: flex; position: relative; align-items: center; color: color-mix(in srgb, var(--hot-pink) 90%, var(--primary-contrast)); background-color: color-mix(in srgb, var(--hot-pink) 5%, transparent); border-radius: 0.5rem; border: 1px solid color-mix(in srgb, var(--hot-pink) 80%, transparent);}.select:hover { background-color: color-mix(in srgb, var(--hot-pink) 15%, transparent);}.select:has([ngComboboxInput][aria-disabled='true']) { opacity: 0.6; cursor: default;}.selected-label-icon { font-size: 1.25rem;}[ngComboboxInput] { opacity: 0; cursor: pointer; padding: 0 3rem; height: 2.5rem; border: none;}[ngCombobox]:focus-within .select { outline: 2px solid color-mix(in srgb, var(--hot-pink) 50%, transparent);}.combobox-label { gap: 1rem; left: 1rem; display: flex; position: absolute; align-items: center; pointer-events: none;}.example-arrow { right: 1rem; position: absolute; pointer-events: none; transition: transform 150ms ease-in-out;}[ngComboboxInput][aria-expanded='true'] ~ .example-arrow { transform: rotate(180deg);}.example-popup-container { width: 100%; padding: 0.5rem; margin-top: 8px; border-radius: 0.5rem; background-color: var(--septenary-contrast); font-size: 0.9rem; max-height: 11rem; opacity: 1; visibility: visible; transition: max-height 150ms ease-out, visibility 0s, opacity 25ms ease-out;}[ngListbox] { gap: 2px; height: 100%; display: flex; overflow: auto; flex-direction: column;}[ngCombobox]:has([ngComboboxInput][aria-expanded='false']) .example-popup-container { max-height: 0; opacity: 0; visibility: hidden; transition: max-height 150ms ease-in, visibility 0s 150ms, opacity 150ms ease-in;}[ngCombobox]:has([ngComboboxInput][aria-expanded='true']) [ngListbox] { display: flex;}[ngOption] { display: flex; cursor: pointer; align-items: center; margin: 1px; padding: 0 1rem; min-height: 2.25rem; border-radius: 0.5rem;}[ngOption]:hover { background-color: color-mix(in srgb, var(--primary-contrast) 5%, transparent);}[ngOption][data-active='true'] { outline-offset: -2px; outline: 2px solid color-mix(in srgb, var(--hot-pink) 50%, transparent);}[ngOption][aria-selected='true'] { color: var(--hot-pink); background-color: color-mix(in srgb, var(--hot-pink) 5%, transparent);}.example-option-icon { font-size: 1.25rem; padding-right: 1rem;}[ngOption]:not([aria-selected='true']) .example-option-check { display: none;}.example-option-icon,.example-option-check { font-size: 0.9rem;}.example-option-text { flex: 1;}
app.ts
import { Combobox, ComboboxInput, ComboboxPopup, ComboboxPopupContainer,} from '@angular/aria/combobox';import {Listbox, Option} from '@angular/aria/listbox';import { afterRenderEffect, ChangeDetectionStrategy, Component, computed, signal, viewChild, viewChildren,} from '@angular/core';import {OverlayModule} from '@angular/cdk/overlay';@Component({ selector: 'app-root', templateUrl: './app.html', styleUrl: './app.css', imports: [ Combobox, ComboboxInput, ComboboxPopup, ComboboxPopupContainer, Listbox, Option, OverlayModule, ], changeDetection: ChangeDetectionStrategy.OnPush,})export class App { /** The combobox listbox popup. */ listbox = viewChild<Listbox<string>>(Listbox); /** The options available in the listbox. */ options = viewChildren<Option<string>>(Option); /** A reference to the ng aria combobox. */ combobox = viewChild<Combobox<string>>(Combobox); /** The icon that is displayed in the combobox. */ displayIcon = computed(() => { const values = this.listbox()?.values() || []; const label = this.labels.find((label) => label.value === values[0]); return label ? label.icon : ''; }); /** The string that is displayed in the combobox. */ displayValue = computed(() => { const values = this.listbox()?.values() || []; return values.length ? values[0] : 'Select a label'; }); /** The labels that are available for selection. */ labels = [ {value: 'Important', icon: 'label'}, {value: 'Starred', icon: 'star'}, {value: 'Work', icon: 'work'}, {value: 'Personal', icon: 'person'}, {value: 'To Do', icon: 'checklist'}, {value: 'Later', icon: 'schedule'}, {value: 'Read', icon: 'menu_book'}, {value: 'Travel', icon: 'flight'}, ]; constructor() { // Scrolls to the active item when the active option changes. // The slight delay here is to ensure animations are done before scrolling. afterRenderEffect(() => { const option = this.options().find((opt) => opt.active()); setTimeout(() => option?.element.scrollIntoView({block: 'nearest'}), 50); }); // Resets the listbox scroll position when the combobox is closed. afterRenderEffect(() => { if (!this.combobox()?.expanded()) { setTimeout(() => this.listbox()?.element.scrollTo(0, 0), 150); } }); }}
app.html
<div ngCombobox class="material-select" readonly> <div #origin class="select"> <span class="combobox-label"> <span class="selected-label-icon material-symbols-outlined" translate="no">{{ displayIcon() }}</span> <span class="selected-label-text">{{ displayValue() }}</span> </span> <input aria-label="Label dropdown" placeholder="Select a label" ngComboboxInput /> <span class="example-arrow material-symbols-outlined" translate="no">arrow_drop_down</span> </div> <ng-template ngComboboxPopupContainer> <ng-template [cdkConnectedOverlay]="{origin, usePopover: 'inline', matchWidth: true}" [cdkConnectedOverlayOpen]="true" > <div class="example-popup-container"> <div ngListbox> @for (label of labels; track label.value) { <div ngOption [value]="label.value" [label]="label.value"> <span class="example-option-icon material-symbols-outlined" translate="no">{{label.icon}}</span> <span class="example-option-text">{{label.value}}</span> <span class="example-option-check material-symbols-outlined" translate="no">check</span> </div> } </div> </div> </ng-template> </ng-template></div>
app.css
@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); --on-primary: var(--page-background);}.docs-light-mode { --on-primary: #fff;}.select { display: flex; position: relative; align-items: center; border-radius: 3rem; color: var(--on-primary); background-color: var(--primary); border: 1px solid color-mix(in srgb, var(--primary) 80%, transparent);}.select:hover { background-color: color-mix(in srgb, var(--primary) 90%, transparent);}.select:has([ngComboboxInput][aria-disabled='true']) { opacity: 0.6; cursor: default;}.selected-label-icon { font-size: 1.25rem;}[ngComboboxInput] { opacity: 0; border: none; cursor: pointer; height: 3rem; padding: 0 3rem;}[ngCombobox]:focus-within .select { outline: 2px solid var(--primary); outline-offset: 2px;}.combobox-label { gap: 1rem; left: 1rem; display: flex; position: absolute; align-items: center; pointer-events: none;}.example-arrow { right: 1rem; position: absolute; pointer-events: none; transition: transform 150ms ease-in-out;}[ngComboboxInput][aria-expanded='true'] ~ .example-arrow { transform: rotate(180deg);}.example-popup-container { width: 100%; padding: 0.5rem; margin-top: 8px; border-radius: 2rem; background-color: var(--septenary-contrast); font-size: 0.9rem; max-height: 13rem; opacity: 1; visibility: visible; transition: max-height 150ms ease-out, visibility 0s, opacity 25ms ease-out;}[ngListbox] { gap: 2px; padding: 2px; height: 100%; display: flex; overflow: auto; flex-direction: column;}[ngCombobox]:has([ngComboboxInput][aria-expanded='false']) .example-popup-container { max-height: 0; opacity: 0; visibility: hidden; transition: max-height 150ms ease-in, visibility 0s 150ms, opacity 150ms ease-in;}[ngCombobox]:has([ngComboboxInput][aria-expanded='true']) [ngListbox] { display: flex;}[ngOption] { display: flex; cursor: pointer; align-items: center; padding: 0 1rem; min-height: 3rem; border-radius: 3rem;}[ngOption]:hover,[ngOption][data-active='true'] { background-color: color-mix(in srgb, var(--primary-contrast) 5%, transparent);}[ngOption][data-active='true'] { outline-offset: -2px; outline: 2px solid var(--primary);}[ngOption][aria-selected='true'] { color: var(--primary); background-color: color-mix(in srgb, var(--primary) 10%, transparent);}.example-option-icon { font-size: 1.25rem; padding-right: 1rem;}[ngOption]:not([aria-selected='true']) .example-option-check { display: none;}.example-option-icon,.example-option-check { font-size: 0.9rem;}.example-option-text { flex: 1;}
app.ts
import { Combobox, ComboboxInput, ComboboxPopup, ComboboxPopupContainer,} from '@angular/aria/combobox';import {Listbox, Option} from '@angular/aria/listbox';import { afterRenderEffect, ChangeDetectionStrategy, Component, computed, signal, viewChild, viewChildren,} from '@angular/core';import {OverlayModule} from '@angular/cdk/overlay';@Component({ selector: 'app-root', templateUrl: './app.html', styleUrl: './app.css', imports: [ Combobox, ComboboxInput, ComboboxPopup, ComboboxPopupContainer, Listbox, Option, OverlayModule, ], changeDetection: ChangeDetectionStrategy.OnPush,})export class App { /** The combobox listbox popup. */ listbox = viewChild<Listbox<string>>(Listbox); /** The options available in the listbox. */ options = viewChildren<Option<string>>(Option); /** A reference to the ng aria combobox. */ combobox = viewChild<Combobox<string>>(Combobox); /** The icon that is displayed in the combobox. */ displayIcon = computed(() => { const values = this.listbox()?.values() || []; const label = this.labels.find((label) => label.value === values[0]); return label ? label.icon : ''; }); /** The string that is displayed in the combobox. */ displayValue = computed(() => { const values = this.listbox()?.values() || []; return values.length ? values[0] : 'Select a label'; }); /** The labels that are available for selection. */ labels = [ {value: 'Important', icon: 'label'}, {value: 'Starred', icon: 'star'}, {value: 'Work', icon: 'work'}, {value: 'Personal', icon: 'person'}, {value: 'To Do', icon: 'checklist'}, {value: 'Later', icon: 'schedule'}, {value: 'Read', icon: 'menu_book'}, {value: 'Travel', icon: 'flight'}, ]; constructor() { // Scrolls to the active item when the active option changes. // The slight delay here is to ensure animations are done before scrolling. afterRenderEffect(() => { const option = this.options().find((opt) => opt.active()); setTimeout(() => option?.element.scrollIntoView({block: 'nearest'}), 50); }); // Resets the listbox scroll position when the combobox is closed. afterRenderEffect(() => { if (!this.combobox()?.expanded()) { setTimeout(() => this.listbox()?.element.scrollTo(0, 0), 150); } }); }}
app.html
<div ngCombobox class="retro-select" readonly> <div #origin class="select"> <span class="combobox-label"> <span class="selected-label-icon material-symbols-outlined" translate="no">{{ displayIcon() }}</span> <span class="selected-label-text">{{ displayValue() }}</span> </span> <input aria-label="Label dropdown" placeholder="Select a label" ngComboboxInput /> <span class="example-arrow material-symbols-outlined" translate="no">arrow_drop_down</span> </div> <ng-template ngComboboxPopupContainer> <ng-template [cdkConnectedOverlay]="{origin, usePopover: 'inline', matchWidth: true}" [cdkConnectedOverlayOpen]="true" > <div class="example-popup-container"> <div ngListbox> @for (label of labels; track label.value) { <div ngOption [value]="label.value" [label]="label.value"> <span class="example-option-icon material-symbols-outlined" translate="no">{{label.icon}}</span> <span class="example-option-text">{{label.value}}</span> <span class="example-option-check material-symbols-outlined" translate="no">check</span> </div> } </div> </div> </ng-template> </ng-template></div>
app.css
@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.8rem; font-family: 'Press Start 2P'; --retro-button-color: color-mix(in srgb, var(--hot-pink) 80%, var(--gray-1000)); --retro-shadow-light: color-mix(in srgb, var(--retro-button-color) 90%, #fff); --retro-shadow-dark: color-mix(in srgb, var(--retro-button-color) 90%, #000); --retro-elevated-shadow: inset 4px 4px 0px 0px var(--retro-shadow-light), inset -4px -4px 0px 0px var(--retro-shadow-dark), 4px 0px 0px 0px var(--gray-700), 0px 4px 0px 0px var(--gray-700), -4px 0px 0px 0px var(--gray-700), 0px -4px 0px 0px var(--gray-700); --retro-flat-shadow: 4px 0px 0px 0px var(--gray-700), 0px 4px 0px 0px var(--gray-700), -4px 0px 0px 0px var(--gray-700), 0px -4px 0px 0px var(--gray-700); --retro-clickable-shadow: inset 4px 4px 0px 0px var(--retro-shadow-light), inset -4px -4px 0px 0px var(--retro-shadow-dark), 4px 0px 0px 0px var(--gray-700), 0px 4px 0px 0px var(--gray-700), -4px 0px 0px 0px var(--gray-700), 0px -4px 0px 0px var(--gray-700), 8px 8px 0px 0px var(--gray-700); --retro-pressed-shadow: inset 4px 4px 0px 0px var(--retro-shadow-dark), inset -4px -4px 0px 0px var(--retro-shadow-light), 4px 0px 0px 0px var(--gray-700), 0px 4px 0px 0px var(--gray-700), -4px 0px 0px 0px var(--gray-700), 0px -4px 0px 0px var(--gray-700), 0px 0px 0px 0px var(--gray-700);}.select { display: flex; position: relative; align-items: center; color: var(--page-background); background-color: var(--hot-pink); box-shadow: var(--retro-clickable-shadow);}.select:hover,.select:focus-within { transform: translate(1px, 1px);}.select:active { transform: translate(4px, 4px); box-shadow: var(--retro-pressed-shadow); background-color: color-mix(in srgb, var(--retro-button-color) 60%, var(--gray-50));}.select:has([ngComboboxInput][aria-disabled='true']) { opacity: 0.6; cursor: default;}.selected-label-icon { font-size: 1.25rem;}[ngComboboxInput] { opacity: 0; cursor: pointer; padding: 0 4rem; height: 2.5rem; border: none;}.select:has([ngComboboxInput][aria-expanded='false']):focus-within { outline-offset: 8px; outline: 4px dashed var(--hot-pink);}.combobox-label { gap: 1rem; left: 1rem; display: flex; position: absolute; align-items: center; pointer-events: none;}.example-arrow { right: 1rem; position: absolute; pointer-events: none; transition: transform 150ms ease-in-out;}[ngComboboxInput][aria-expanded='true'] ~ .example-arrow { transform: rotate(180deg);}.example-popup-container { width: 100%; padding: 0.5rem; margin-top: 20px; box-shadow: var(--retro-flat-shadow); background-color: var(--septenary-contrast); max-height: 11rem; opacity: 1; visibility: visible; transition: max-height 150ms ease-out, visibility 0s, opacity 25ms ease-out;}[ngListbox] { gap: 2px; height: 100%; display: flex; overflow: auto; flex-direction: column;}[ngCombobox]:has([ngComboboxInput][aria-expanded='false']) .example-popup-container { max-height: 0; opacity: 0; visibility: hidden; transition: max-height 150ms ease-in, visibility 0s 150ms, opacity 150ms ease-in;}[ngCombobox]:has([ngComboboxInput][aria-expanded='true']) [ngListbox] { display: flex;}[ngOption] { display: flex; cursor: pointer; align-items: center; padding: 0 1rem; font-size: 0.6rem; min-height: 2.25rem;}[ngOption]:hover { background-color: color-mix(in srgb, var(--primary-contrast) 5%, transparent);}[ngOption][data-active='true'] { outline-offset: -2px; outline: 2px dashed var(--hot-pink);}[ngOption][aria-selected='true'] { color: var(--hot-pink); background-color: color-mix(in srgb, var(--hot-pink) 5%, transparent);}.example-option-icon { font-size: 1.25rem; padding-right: 1rem;}[ngOption]:not([aria-selected='true']) .example-option-check { display: none;}.example-option-icon,.example-option-check { font-size: 0.9rem;}.example-option-text { flex: 1;}
readonly属性は、入力フィールドへの入力を防ぎます。ポップアップはクリックまたは矢印キーで開きます。ユーザーはキーボードでオプションを移動し、Enterキーかクリックで選択します。
この設定は、SelectおよびMultiselectパターンの基盤を提供します。トリガーとオーバーレイの位置決めを含む完全なドロップダウンの実装については、これらのガイドを参照してください。
ダイアログポップアップ
ポップアップには、背景とフォーカストラップを備えたモーダルな動作が必要な場合があります。コンボボックスダイアログディレクティブは、特殊なユースケースのためにこのパターンを提供します。
app.ts
import { Combobox, ComboboxDialog, ComboboxInput, ComboboxPopupContainer,} from '@angular/aria/combobox';import {Listbox, Option} from '@angular/aria/listbox';import {afterRenderEffect, Component, computed, signal, untracked, viewChild} from '@angular/core';import {FormsModule} from '@angular/forms';@Component({ selector: 'app-root', templateUrl: './app.html', styleUrl: './app.css', imports: [ ComboboxDialog, Combobox, ComboboxInput, ComboboxPopupContainer, Listbox, Option, FormsModule, ],})export class App { dialog = viewChild(ComboboxDialog); listbox = viewChild<Listbox<string>>(Listbox); combobox = viewChild<Combobox<string>>(Combobox); value = signal(''); searchString = signal(''); options = computed(() => ALL_COUNTRIES.filter((country) => country.toLowerCase().startsWith(this.searchString().toLowerCase()), ), ); selectedCountries = signal<string[]>([]); constructor() { afterRenderEffect(() => { if (this.dialog() && this.combobox()?.expanded()) { untracked(() => this.listbox()?.gotoFirst()); this.positionDialog(); } }); afterRenderEffect(() => { if (this.selectedCountries().length > 0) { untracked(() => this.dialog()?.close()); this.value.set(this.selectedCountries()[0]); this.searchString.set(''); } }); afterRenderEffect(() => this.listbox()?.scrollActiveItemIntoView()); } // TODO(wagnermaciel): Switch to using the CDK for positioning. positionDialog() { const dialog = this.dialog()!; const combobox = this.combobox()!; const comboboxRect = combobox.inputElement()?.getBoundingClientRect(); const scrollY = window.scrollY; if (comboboxRect) { dialog.element.style.width = `${comboboxRect.width}px`; dialog.element.style.top = `${comboboxRect.bottom + scrollY + 4}px`; dialog.element.style.left = `${comboboxRect.left - 1}px`; } }}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',];
app.html
<div ngCombobox #combobox="ngCombobox" [readonly]="true"> <div class="combobox-input-container"> <input ngComboboxInput placeholder="Select a country..." [value]="value()" /> <span class="material-symbols-outlined icon arrow-icon" translate="no">arrow_drop_down</span> </div> <ng-template ngComboboxPopupContainer> <dialog ngComboboxDialog class="dialog"> <div ngCombobox #combobox="ngCombobox" filterMode="manual" [alwaysExpanded]="true"> <div class="combobox-input-container"> <span class="material-symbols-outlined icon search-icon" translate="no">search</span> <input ngComboboxInput class="combobox-input" placeholder="Search..." [(value)]="searchString" /> </div> <ng-template ngComboboxPopupContainer> @if (options().length === 0) { <div class="no-results">No results found</div> } <div ngListbox [(values)]="selectedCountries"> @for (option of options(); track option) { <div ngOption [value]="option" [label]="option"> <span class="option-label">{{option}}</span> <span class="material-symbols-outlined icon check-icon" translate="no">check</span> </div> } </div> </ng-template> </div> </dialog> </ng-template></div>
app.css
@import url('https://fonts.googleapis.com/icon?family=Material+Symbols+Outlined');:host { display: flex; justify-content: center; font-family: var(--inter-font); --border-color: color-mix(in srgb, var(--full-contrast) 20%, var(--page-background));}[ngCombobox] { position: relative; width: 100%; display: flex; flex-direction: column; border: 1px solid var(--border-color); border-radius: 0.25rem;}[ngCombobox]:has([readonly='true']) { width: 15rem;}.combobox-input-container { display: flex; position: relative; align-items: center; border-radius: 0.25rem;}[ngComboboxInput] { border-radius: 0.25rem;}[ngComboboxInput][readonly='true'] { cursor: pointer; padding: 0.7rem 1rem;}[ngCombobox]:focus-within [ngComboboxInput] { outline: 1.5px solid var(--vivid-pink); box-shadow: 0 0 0 4px color-mix(in srgb, var(--vivid-pink) 25%, transparent);}.icon { width: 24px; height: 24px; font-size: 20px; display: grid; place-items: center; pointer-events: none;}.search-icon { padding: 0 0.5rem; position: absolute; opacity: 0.8;}.arrow-icon { padding: 0 0.5rem; position: absolute; right: 0; opacity: 0.8; transition: transform 0.2s ease;}[ngComboboxInput][aria-expanded='true'] + .arrow-icon { transform: rotate(180deg);}[ngComboboxInput] { width: 100%; border: none; outline: none; font-size: 1rem; padding: 0.7rem 1rem 0.7rem 2.5rem; background-color: var(--mat-sys-surface);}.popover { margin: 0; padding: 0; border: 1px solid var(--border-color); border-radius: 0.25rem; background-color: var(--mat-sys-surface);}[ngListbox] { gap: 2px; max-height: 200px; display: flex; overflow: auto; flex-direction: column;}[ngOption] { display: flex; cursor: pointer; align-items: center; margin: 1px; padding: 0 1rem; min-height: 2.25rem; border-radius: 0.5rem;}[ngOption]:hover { background-color: color-mix(in srgb, var(--primary-contrast) 5%, transparent);}[ngOption][data-active='true'] { outline-offset: -2px; outline: 2px solid var(--vivid-pink);}[ngOption][aria-selected='true'] { color: var(--vivid-pink); background-color: color-mix(in srgb, var(--vivid-pink) 5%, transparent);}[ngOption]:not([aria-selected='true']) .check-icon { display: none;}.option-label { flex: 1;}.check-icon { font-size: 0.9rem;}.dialog { position: absolute; left: auto; right: auto; top: auto; bottom: auto; padding: 0; border: 1px solid var(--border-color); border-radius: 0.25rem;}.dialog .combobox-input-container { border-radius: 0;}.dialog [ngCombobox],.dialog .combobox-input-container { border: none;}.dialog [ngComboboxInput] { border-bottom-left-radius: 0; border-bottom-right-radius: 0;}.dialog [ngCombobox]:focus-within [ngComboboxInput] { outline: none; box-shadow: none;}.dialog .combobox-input-container { border-bottom: 1px solid var(--border-color);}.dialog::backdrop { opacity: 0;}.no-results { padding: 1rem;}
app.ts
import { Combobox, ComboboxDialog, ComboboxInput, ComboboxPopupContainer,} from '@angular/aria/combobox';import {Listbox, Option} from '@angular/aria/listbox';import {afterRenderEffect, Component, computed, signal, untracked, viewChild} from '@angular/core';import {FormsModule} from '@angular/forms';@Component({ selector: 'app-root', templateUrl: './app.html', styleUrl: './app.css', imports: [ ComboboxDialog, Combobox, ComboboxInput, ComboboxPopupContainer, Listbox, Option, FormsModule, ],})export class App { dialog = viewChild(ComboboxDialog); listbox = viewChild<Listbox<string>>(Listbox); combobox = viewChild<Combobox<string>>(Combobox); value = signal(''); searchString = signal(''); options = computed(() => ALL_COUNTRIES.filter((country) => country.toLowerCase().startsWith(this.searchString().toLowerCase()), ), ); selectedCountries = signal<string[]>([]); constructor() { afterRenderEffect(() => { if (this.dialog() && this.combobox()?.expanded()) { untracked(() => this.listbox()?.gotoFirst()); this.positionDialog(); } }); afterRenderEffect(() => { if (this.selectedCountries().length > 0) { untracked(() => this.dialog()?.close()); this.value.set(this.selectedCountries()[0]); this.searchString.set(''); } }); afterRenderEffect(() => this.listbox()?.scrollActiveItemIntoView()); } // TODO(wagnermaciel): Switch to using the CDK for positioning. positionDialog() { const dialog = this.dialog()!; const combobox = this.combobox()!; const comboboxRect = combobox.inputElement()?.getBoundingClientRect(); const scrollY = window.scrollY; if (comboboxRect) { dialog.element.style.width = `${comboboxRect.width}px`; dialog.element.style.top = `${comboboxRect.bottom + scrollY + 4}px`; dialog.element.style.left = `${comboboxRect.left - 1}px`; } }}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',];
app.html
<div ngCombobox #combobox="ngCombobox" [readonly]="true" class="material-combobox"> <div class="combobox-input-container"> <input ngComboboxInput placeholder="Select a country..." [value]="value()" /> <span class="material-symbols-outlined icon arrow-icon" translate="no">arrow_drop_down</span> </div> <ng-template ngComboboxPopupContainer> <dialog ngComboboxDialog class="dialog"> <div ngCombobox #combobox="ngCombobox" filterMode="manual" [alwaysExpanded]="true"> <div class="combobox-input-container"> <span class="material-symbols-outlined icon search-icon" translate="no">search</span> <input ngComboboxInput class="combobox-input" placeholder="Search..." [(value)]="searchString" /> </div> <ng-template ngComboboxPopupContainer> @if (options().length === 0) { <div class="no-results">No results found</div> } <div ngListbox [(values)]="selectedCountries"> @for (option of options(); track option) { <div ngOption [value]="option" [label]="option"> <span class="option-label">{{option}}</span> <span class="material-symbols-outlined icon check-icon" translate="no">check</span> </div> } </div> </ng-template> </div> </dialog> </ng-template></div>
app.css
@import url('https://fonts.googleapis.com/icon?family=Material+Symbols+Outlined');:host { display: flex; justify-content: center; font-family: var(--inter-font); --border-color: color-mix(in srgb, var(--full-contrast) 20%, var(--page-background));}[ngCombobox] { position: relative; width: 100%; display: flex; flex-direction: column; border: 1px solid var(--border-color); border-radius: 1rem;}[ngCombobox]:has([readonly='true']) { width: 15rem;}.combobox-input-container { display: flex; position: relative; align-items: center; border-radius: 1rem;}[ngComboboxInput] { border-radius: 1rem;}[ngComboboxInput][readonly='true'] { cursor: pointer; padding: 0.7rem 1rem;}[ngCombobox]:focus-within [ngComboboxInput] { outline: 1.5px solid var(--vivid-pink); box-shadow: 0 0 0 4px color-mix(in srgb, var(--vivid-pink) 25%, transparent);}.icon { width: 24px; height: 24px; font-size: 20px; display: grid; place-items: center; pointer-events: none;}.search-icon { padding: 0 0.5rem; position: absolute; opacity: 0.8;}.arrow-icon { padding: 0 0.5rem; position: absolute; right: 0; opacity: 0.8; transition: transform 0.2s ease;}[ngComboboxInput][aria-expanded='true'] + .arrow-icon { transform: rotate(180deg);}[ngComboboxInput] { width: 100%; border: none; outline: none; font-size: 1rem; padding: 0.7rem 1rem 0.7rem 2.5rem; background-color: var(--mat-sys-surface);}[ngListbox] { gap: 2px; max-height: 10rem; display: flex; overflow: auto; flex-direction: column;}[ngOption] { display: flex; cursor: pointer; align-items: center; margin: 1px; padding: 1rem; min-height: 1rem; border-radius: 1rem;}[ngOption]:hover,[ngOption][data-active='true'] { background-color: color-mix(in srgb, var(--primary-contrast) 5%, transparent);}[ngOption][data-active='true'] { outline-offset: -2px; outline: 2px solid var(--primary);}[ngOption][aria-selected='true'] { color: var(--primary); background-color: color-mix(in srgb, var(--primary) 10%, transparent);}[ngOption]:not([aria-selected='true']) .check-icon { display: none;}.option-label { flex: 1;}.check-icon { font-size: 0.9rem;}.dialog { padding: none; position: absolute; left: auto; right: auto; top: auto; bottom: auto; border: 1px solid var(--border-color); border-radius: 1rem;}.dialog .combobox-input-container { border-radius: 0;}.dialog [ngCombobox],.dialog .combobox-input-container { border: none;}.dialog [ngComboboxInput] { border-bottom-left-radius: 0; border-bottom-right-radius: 0;}.dialog [ngCombobox]:focus-within [ngComboboxInput] { outline: none; box-shadow: none;}.dialog::backdrop { opacity: 0;}.no-results { padding: 1rem;}
app.ts
import { Combobox, ComboboxDialog, ComboboxInput, ComboboxPopupContainer,} from '@angular/aria/combobox';import {Listbox, Option} from '@angular/aria/listbox';import {afterRenderEffect, Component, computed, signal, untracked, viewChild} from '@angular/core';import {FormsModule} from '@angular/forms';@Component({ selector: 'app-root', templateUrl: './app.html', styleUrl: './app.css', imports: [ ComboboxDialog, Combobox, ComboboxInput, ComboboxPopupContainer, Listbox, Option, FormsModule, ],})export class App { dialog = viewChild(ComboboxDialog); listbox = viewChild<Listbox<string>>(Listbox); combobox = viewChild<Combobox<string>>(Combobox); value = signal(''); searchString = signal(''); options = computed(() => ALL_COUNTRIES.filter((country) => country.toLowerCase().startsWith(this.searchString().toLowerCase()), ), ); selectedCountries = signal<string[]>([]); constructor() { afterRenderEffect(() => { if (this.dialog() && this.combobox()?.expanded()) { untracked(() => this.listbox()?.gotoFirst()); this.positionDialog(); } }); afterRenderEffect(() => { if (this.selectedCountries().length > 0) { untracked(() => this.dialog()?.close()); this.value.set(this.selectedCountries()[0]); this.searchString.set(''); } }); afterRenderEffect(() => this.listbox()?.scrollActiveItemIntoView()); } // TODO(wagnermaciel): Switch to using the CDK for positioning. positionDialog() { const dialog = this.dialog()!; const combobox = this.combobox()!; const comboboxRect = combobox.inputElement()?.getBoundingClientRect(); const scrollY = window.scrollY; if (comboboxRect) { dialog.element.style.width = `${comboboxRect.width}px`; dialog.element.style.top = `${comboboxRect.bottom + scrollY + 4}px`; dialog.element.style.left = `${comboboxRect.left - 1}px`; } }}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',];
app.html
<div ngCombobox #combobox="ngCombobox" [readonly]="true" class="retro-combobox"> <div class="combobox-input-container"> <input ngComboboxInput placeholder="Select a country..." [value]="value()" /> <span class="material-symbols-outlined icon arrow-icon" translate="no">arrow_drop_down</span> </div> <ng-template ngComboboxPopupContainer> <dialog ngComboboxDialog class="dialog"> <div ngCombobox #combobox="ngCombobox" filterMode="manual" [alwaysExpanded]="true"> <div class="combobox-input-container"> <span class="material-symbols-outlined icon search-icon" translate="no">search</span> <input ngComboboxInput class="combobox-input" placeholder="Search..." [(value)]="searchString" /> </div> <ng-template ngComboboxPopupContainer> <div ngListbox [(values)]="selectedCountries"> @for (option of options(); track option) { <div ngOption [value]="option" [label]="option"> <span>{{option}}</span> <span class="material-symbols-outlined icon selected-icon" translate="no">check</span> </div> } </div> </ng-template> </div> </dialog> </ng-template></div>
app.css
@import url('https://fonts.googleapis.com/icon?family=Material+Symbols+Outlined');@import url('https://fonts.googleapis.com/css2?family=Press+Start+2P&display=swap');:host { display: flex; justify-content: center; font-size: 0.6rem; font-family: 'Press Start 2P'; --retro-button-color: #fff; --retro-shadow-light: color-mix(in srgb, var(--retro-button-color) 90%, #fff); --retro-shadow-dark: color-mix(in srgb, var(--retro-button-color) 90%, #000); --retro-elevated-shadow: inset 4px 4px 0px 0px var(--retro-shadow-light), inset -4px -4px 0px 0px var(--retro-shadow-dark), 4px 0px 0px 0px var(--gray-700), 0px 4px 0px 0px var(--gray-700), -4px 0px 0px 0px var(--gray-700), 0px -4px 0px 0px var(--gray-700); --retro-flat-shadow: 4px 0px 0px 0px var(--gray-700), 0px 4px 0px 0px var(--gray-700), -4px 0px 0px 0px var(--gray-700), 0px -4px 0px 0px var(--gray-700); --retro-pressed-shadow: inset 4px 4px 0px 0px var(--retro-shadow-dark), inset -4px -4px 0px 0px var(--retro-shadow-light), 4px 0px 0px 0px var(--gray-700), 0px 4px 0px 0px var(--gray-700), -4px 0px 0px 0px var(--gray-700), 0px -4px 0px 0px var(--gray-700), 0px 0px 0px 0px var(--gray-700);}[ngComboboxInput] { width: 15rem; font-size: 0.6rem; border-radius: 0; font-family: 'Press Start 2P'; word-spacing: -5px; padding: 0.75rem 0.5rem 0.75rem 2.5rem; color: #000; border: none;}[ngComboboxInput]::placeholder { color: #000; opacity: 0.7;}[ngCombobox]:has([readonly='true']) { width: 15rem; box-shadow: var(--retro-flat-shadow); background-color: var(--retro-button-color);}.combobox-input-container { display: flex; position: relative; align-items: center;}[ngComboboxInput][readonly='true'] { cursor: pointer; padding: 0.7rem 1rem;}[ngCombobox]:focus-within [ngComboboxInput] { outline: 1.5px solid var(--vivid-pink); box-shadow: 0 0 0 4px color-mix(in srgb, var(--vivid-pink) 25%, transparent);}.icon { width: 24px; height: 24px; font-size: 20px; display: grid; place-items: center; pointer-events: none;}.search-icon { padding: 0 0.5rem; position: absolute; opacity: 0.8;}.arrow-icon { padding: 0 0.5rem; position: absolute; right: 0; opacity: 0.8; transition: transform 0.2s ease;}[ngComboboxInput][aria-expanded='true'] + .arrow-icon { transform: rotate(180deg);}[ngListbox] { display: flex; flex-direction: column; overflow: auto; max-height: 10rem; padding: 0.5rem; gap: 4px; font-size: 0.9rem;}[ngOption] { cursor: pointer; padding: 0.3rem 1rem; display: flex; overflow: hidden; flex-shrink: 0; align-items: center; justify-content: space-between; gap: 1rem; font-size: 0.6rem;}.checkbox-blank-icon,[ngOption][aria-selected='true'] .checkbox-filled-icon { display: flex; align-items: center;}.checkbox-filled-icon,[ngOption][aria-selected='true'] .checkbox-blank-icon { display: none;}.checkbox-blank-icon { opacity: 0.6;}.selected-icon { visibility: hidden;}[ngOption][aria-selected='true'] .selected-icon { visibility: visible;}[ngOption][aria-selected='true'] { color: var(--vivid-pink); background-color: color-mix(in srgb, var(--vivid-pink) 10%, transparent);}[ngOption]:hover { background-color: color-mix(in srgb, var(--mat-sys-on-surface) 10%, transparent);}[ngCombobox]:focus-within [data-active='true'] { outline: 2px solid color-mix(in srgb, var(--vivid-pink) 80%, transparent);}.dialog { margin-top: 8px; position: absolute; left: auto; right: auto; top: auto; bottom: auto; padding: 0; border: none; box-shadow: var(--retro-flat-shadow);}.dialog .combobox-input-container { border-radius: 0;}.dialog [ngCombobox],.dialog .combobox-input-container { border: none;}.dialog [ngComboboxInput] { border-bottom: 4px solid #000;}.dialog [ngCombobox]:focus-within [ngComboboxInput] { outline: none; box-shadow: none;}.dialog::backdrop { opacity: 0;}
ngComboboxDialogディレクティブは、ネイティブのdialog要素を使用してモーダルポップアップを作成します。これにより、背景の動作とフォーカストラップが提供されます。選択インターフェースがモーダルなインタラクションを必要とする場合や、ポップアップのコンテンツがフルスクリーンのフォーカスを必要とするほど複雑な場合に、ダイアログポップアップを使用します。
API
Comboboxディレクティブ
ngComboboxディレクティブは、テキスト入力とポップアップを連携させます。
入力
| プロパティ | 型 | デフォルト | 説明 |
|---|---|---|---|
filterMode |
'manual' | 'auto-select' | 'highlight' |
'manual' |
選択の動作を制御します |
disabled |
boolean |
false |
コンボボックスを無効にします |
readonly |
boolean |
false |
コンボボックスを読み取り専用にします(Select/Multiselect用) |
firstMatch |
V |
- | 自動選択のために、最初に一致した項目の値 |
alwaysExpanded |
boolean |
false |
ポップアップを常に開いたままにします |
フィルターモード:
'manual'- ユーザーがフィルタリングと選択を明示的に制御します。ポップアップには、あなたのフィルタリングロジックに基づいたオプションが表示されます。ユーザーはEnterキーまたはクリックで選択します。このモードは最も柔軟性があります。'auto-select'- ユーザーが入力すると、入力値は最初に一致したオプションに自動的に更新されます。連携のためにfirstMatch入力が必要です。例についてはオートコンプリートガイドを参照してください。'highlight'- 入力値を変更せずに、一致するテキストをハイライトします。ユーザーは矢印キーで移動し、Enterキーで選択します。
シグナル
| プロパティ | 型 | 説明 |
|---|---|---|
expanded |
Signal<boolean> |
ポップアップが現在開いているかどうか |
メソッド
| メソッド | パラメータ | 説明 |
|---|---|---|
open |
なし | コンボボックスを開きます |
close |
なし | コンボボックスを閉じます |
expand |
なし | コンボボックスを展開します |
collapse |
なし | コンボボックスを折りたたみます |
ComboboxInputディレクティブ
ngComboboxInputディレクティブは、入力要素をコンボボックスに接続します。
モデル
| プロパティ | 型 | 説明 |
|---|---|---|
value |
string |
[(value)]を使用した双方向バインディング可能な値 |
入力要素は、キーボード操作とARIA属性を自動的に受け取ります。
ComboboxPopupディレクティブ
ngComboboxPopupディレクティブ(ホストディレクティブ)は、ポップアップの可視性と連携を管理します。通常、ng-template内のngComboboxPopupContainerまたはCDK Overlayと一緒に使用されます。
ComboboxPopupContainerディレクティブ
ngComboboxPopupContainerディレクティブは、ng-templateをポップアップのコンテンツとしてマークします。
<ng-template ngComboboxPopupContainer> <div ngListbox>...</div></ng-template>
Popover APIまたはCDK Overlayと一緒に使用して、位置決めします。
ComboboxDialogディレクティブ
ngComboboxDialogディレクティブは、モーダルなコンボボックスポップアップを作成します。
<dialog ngComboboxDialog> <div ngListbox>...</div></dialog>
背景とフォーカストラップを備えたモーダルポップアップの動作に使用します。
関連するパターンとディレクティブ
Comboboxは、これらのドキュメント化されたパターンのためのプリミティブディレクティブです:
- オートコンプリート - フィルタリングと提案のパターン(フィルターモード付きのComboboxを使用)
- セレクト - 単一選択のドロップダウンパターン(
readonly付きのComboboxを使用) - マルチセレクト - 複数選択のパターン(
readonly+ 複数選択が有効なListbox付きのComboboxを使用)
Comboboxは通常、以下と組み合わせて使用されます: