這個指令會顯示一份選項清單供使用者選取,支援鍵盤導覽、單選或多選,以及螢幕閱讀器支援。
import {Listbox, Option} from '@angular/aria/listbox';
import {Component} from '@angular/core';
@Component({
selector: 'app-root',
templateUrl: './app.html',
styleUrl: './app.css',
imports: [Listbox, Option],
})
export class App {
/** The options available in the listbox. */
options = [
'Option 1',
'Option 2',
'Option 3',
'Option 4',
'Option 5',
'Option 6',
'Option 7',
'Option 8',
];
}
<div class="listbox-container">
<div ngListbox [value]="['Option 1']">
@for (option of options; track option) {
<div ngOption [value]="option">
<span class="example-option-text">{{ option }}</span>
<span
class="example-option-check material-symbols-outlined"
translate="no"
aria-hidden="true"
>check</span
>
</div>
}
</div>
</div>
@import url('https://fonts.googleapis.com/icon?family=Material+Symbols+Outlined');
:host {
display: flex;
justify-content: center;
font-family: var(--inter-font);
}
.listbox-container {
width: 200px;
height: 11rem;
padding: 0.5rem;
border-radius: 0.5rem;
background-color: var(--septenary-contrast);
font-size: 0.9rem;
}
[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 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);
}
[ngOption]:not([aria-selected='true']) .example-option-check {
display: none;
}
.example-option-check {
font-size: 0.9rem;
}
.example-option-text {
flex: 1;
}
import {Listbox, Option} from '@angular/aria/listbox';
import {Component} from '@angular/core';
@Component({
selector: 'app-root',
templateUrl: './app.html',
styleUrl: './app.css',
imports: [Listbox, Option],
})
export class App {
/** The options available in the listbox. */
options = [
'Option 1',
'Option 2',
'Option 3',
'Option 4',
'Option 5',
'Option 6',
'Option 7',
'Option 8',
];
}
<div class="material-listbox">
<div ngListbox>
@for (option of options; track option) {
<div ngOption [value]="option">
<span class="example-option-text">{{ option }}</span>
<span
class="example-option-check material-symbols-outlined"
translate="no"
aria-hidden="true"
>check</span
>
</div>
}
</div>
</div>
@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;
}
.material-listbox {
width: 200px;
height: 13rem;
padding: 0.5rem;
border-radius: 2rem;
background-color: var(--septenary-contrast);
font-size: 0.9rem;
}
[ngListbox] {
gap: 2px;
padding: 2px;
height: 100%;
display: flex;
overflow: auto;
flex-direction: column;
}
[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);
}
[ngOption]:not([aria-selected='true']) .example-option-check {
display: none;
}
.example-option-check {
font-size: 0.9rem;
}
.example-option-text {
flex: 1;
}
import {Listbox, Option} from '@angular/aria/listbox';
import {Component} from '@angular/core';
@Component({
selector: 'app-root',
templateUrl: './app.html',
styleUrl: './app.css',
imports: [Listbox, Option],
})
export class App {
/** The options available in the listbox. */
options = [
'Option 1',
'Option 2',
'Option 3',
'Option 4',
'Option 5',
'Option 6',
'Option 7',
'Option 8',
];
}
<div class="retro-listbox">
<div ngListbox>
@for (option of options; track option) {
<div ngOption [value]="option">
<span class="example-option-text">{{ option }}</span>
<span
class="example-option-check material-symbols-outlined"
translate="no"
aria-hidden="true"
>check</span
>
</div>
}
</div>
</div>
@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(--page-background));
--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-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-listbox {
width: 200px;
height: 11rem;
padding: 0.5rem;
box-shadow: var(--retro-flat-shadow);
background-color: var(--septenary-contrast);
}
[ngListbox] {
gap: 2px;
height: 100%;
display: flex;
overflow: auto;
flex-direction: column;
}
[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);
}
[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;
}
Listbox 是 Select、Multiselect 與 Autocomplete 模式所使用的基礎指令。對於大多數下拉清單需求,請改用那些已文件化的模式。
在下列情況可以考慮直接使用 listbox:
- 建置自訂的選取元件:建立具有特定行為的專門介面
- 可見的選取清單:把可選取的項目直接顯示在頁面上(不是放在下拉清單裡)
- 自訂整合模式:與獨特的彈出視窗或版面配置需求整合
在下列情況避免使用 listbox:
- 需要導覽選單:請使用 Menu 指令來處理動作與指令
Angular 的 listbox 提供完整無障礙的清單實作,包含:
- 鍵盤導覽:用方向鍵瀏覽選項、用 Enter 或空白鍵選取
- 螢幕閱讀器支援:內建 ARIA 屬性,包括 role="listbox"
- 單選或多選:
multi 屬性控制選取模式
- 水平或垂直:
orientation 屬性控制版面方向
- 預先輸入搜尋:輸入字元即可跳到相符的選項
- 以訊號為基礎的反應性:使用 Angular 訊號進行反應式狀態管理
應用程式有時需要把可選取的清單直接顯示在頁面上,而不是藏在下拉清單裡。獨立的 listbox 為這些可見的清單介面提供鍵盤導覽與選取功能。
import {Listbox, Option} from '@angular/aria/listbox';
import {Component} from '@angular/core';
@Component({
selector: 'app-root',
templateUrl: './app.html',
styleUrl: './app.css',
imports: [Listbox, Option],
})
export class App {
/** The options available in the listbox. */
options = [
'Option 1',
'Option 2',
'Option 3',
'Option 4',
'Option 5',
'Option 6',
'Option 7',
'Option 8',
];
}
<div class="listbox-container">
<div ngListbox [value]="['Option 1']">
@for (option of options; track option) {
<div ngOption [value]="option">
<span class="example-option-text">{{ option }}</span>
<span
class="example-option-check material-symbols-outlined"
translate="no"
aria-hidden="true"
>check</span
>
</div>
}
</div>
</div>
value 模型訊號提供與已選取項目的雙向繫結。在 selectionMode="explicit" 之下,使用者按空白鍵或 Enter 來選取選項。把 listbox 與 combobox 及覆蓋層定位結合的下拉清單模式,請參閱 Select 模式。
清單有時以水平方式呈現效果更好,例如類似工具列的介面或分頁式的選取。orientation 屬性會同時改變版面配置與鍵盤導覽方向。
import {Listbox, Option} from '@angular/aria/listbox';
import {Component} from '@angular/core';
@Component({
selector: 'app-root',
templateUrl: './app.html',
styleUrl: './app.css',
imports: [Listbox, Option],
})
export class App {
/** The options available in the listbox. */
amenities = ['Washer / Dryer', 'Ramp access', 'Garden', 'Cats OK', 'Dogs OK', 'Smoke-free'];
}
<div ngListbox aria-label="Amenities" orientation="horizontal" selectionMode="explicit" multi>
@for (amenity of amenities; track amenity) {
<div ngOption [value]="amenity" [label]="amenity">
<span class="option-label">{{ amenity }}</span>
</div>
}
</div>
@import url('https://fonts.googleapis.com/icon?family=Material+Symbols+Outlined');
:host {
font-size: 0.8rem;
font-family: var(--inter-font);
}
[ngListbox] {
gap: 0.5rem;
display: flex;
flex-wrap: wrap;
}
[ngOption] {
cursor: pointer;
border-radius: 1rem;
padding: 0.3rem 1rem;
color: var(--hot-pink);
border: 1px solid var(--hot-pink);
background-color: color-mix(in srgb, var(--hot-pink) 5%, transparent);
}
[ngOption]:focus {
outline: 2px solid var(--hot-pink);
outline-offset: 2px;
}
[ngOption]:hover {
background-color: color-mix(in srgb, var(--hot-pink) 15%, transparent);
}
[ngOption][aria-selected='true'] {
color: var(--page-background);
background-color: var(--hot-pink);
}
import {Listbox, Option} from '@angular/aria/listbox';
import {Component} from '@angular/core';
@Component({
selector: 'app-root',
templateUrl: './app.html',
styleUrl: './app.css',
imports: [Listbox, Option],
})
export class App {
/** The options available in the listbox. */
amenities = ['Washer / Dryer', 'Ramp access', 'Garden', 'Cats OK', 'Dogs OK', 'Smoke-free'];
}
<div
ngListbox
class="material-listbox"
aria-label="Amenities"
orientation="horizontal"
selectionMode="explicit"
multi
>
@for (amenity of amenities; track amenity) {
<div ngOption [value]="amenity" [label]="amenity">
<span class="check-icon material-symbols-outlined" translate="no" aria-hidden="true"
>check</span
>
<span class="option-label">{{ amenity }}</span>
</div>
}
</div>
@import url('https://fonts.googleapis.com/icon?family=Material+Symbols+Outlined');
:host {
font-size: 0.8rem;
font-family: var(--inter-font);
}
[ngListbox] {
gap: 0.5rem;
display: flex;
flex-wrap: wrap;
}
[ngOption] {
display: flex;
cursor: pointer;
align-items: center;
border-radius: 0.3rem;
padding: 0.3rem 0.5rem;
color: var(--hot-pink);
border: 1px solid var(--hot-pink);
background-color: color-mix(in srgb, var(--hot-pink) 5%, transparent);
}
[ngOption]:focus {
outline: 2px solid var(--hot-pink);
outline-offset: 2px;
}
[ngOption]:hover {
background-color: color-mix(in srgb, var(--hot-pink) 15%, transparent);
}
[ngOption][aria-selected='true'] {
color: var(--page-background);
background-color: var(--hot-pink);
}
.check-icon {
width: 0;
font-size: 1.25rem;
overflow: hidden;
transition:
width 0.2s ease-in-out,
padding-right 0.2s ease-in-out;
}
[ngOption][aria-selected='true'] .check-icon {
width: 1.5rem;
padding-right: 0.2rem;
}
import {Listbox, Option} from '@angular/aria/listbox';
import {Component} from '@angular/core';
@Component({
selector: 'app-root',
templateUrl: './app.html',
styleUrl: './app.css',
imports: [Listbox, Option],
})
export class App {
/** The options available in the listbox. */
amenities = ['Washer / Dryer', 'Ramp access', 'Garden', 'Cats OK', 'Dogs OK', 'Smoke-free'];
}
<div
ngListbox
class="retro-listbox"
aria-label="Amenities"
orientation="horizontal"
selectionMode="explicit"
multi
>
@for (amenity of amenities; track amenity) {
<div ngOption [value]="amenity" [label]="amenity">
<span class="check-icon material-symbols-outlined" translate="no" aria-hidden="true"
>check</span
>
<span class="option-label">{{ amenity }}</span>
</div>
}
</div>
@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(--page-background));
--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-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-flat-shadow-small:
2px 0px 0px 0px var(--tertiary-contrast), 0px 2px 0px 0px var(--tertiary-contrast),
-2px 0px 0px 0px var(--tertiary-contrast), 0px -2px 0px 0px var(--tertiary-contrast);
}
.retro-listbox {
gap: 0.5rem;
display: flex;
flex-wrap: wrap;
padding: 0.5rem;
box-shadow: var(--retro-flat-shadow);
background-color: var(--septenary-contrast);
}
[ngOption] {
display: flex;
cursor: pointer;
align-items: center;
padding: 0.3rem 0.5rem;
font-size: 0.6rem;
box-shadow: var(--retro-flat-shadow-small);
}
[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);
}
.check-icon {
width: 0;
font-size: 0.9rem;
overflow: hidden;
transition: width 0.2s ease-in-out;
}
[ngOption][aria-selected='true'] .check-icon {
width: 1.2rem;
}
在 orientation="horizontal" 之下,左右方向鍵會在選項之間瀏覽,而不是上下方向鍵。Listbox 會自動處理由右至左(RTL)的語言,做法是反轉導覽方向。
Listbox 支援兩種選取模式,用來控制項目在什麼時候被選取。
'follow' 模式會自動選取取得焦點的項目,在選取頻繁變更時提供更快的互動。'explicit' 模式則需要用空白鍵或 Enter 確認選取,避免在瀏覽時不小心變更。下拉清單模式在單選時通常使用 'follow' 模式。
import {Component} from '@angular/core';
import {Listbox, Option} from '@angular/aria/listbox';
@Component({
selector: 'app-root',
templateUrl: './app.html',
styleUrl: './app.css',
imports: [Listbox, Option],
})
export class App {
amenities = ['Washer / Dryer', 'Ramp access', 'Garden', 'Cats OK', 'Dogs OK', 'Smoke-free'];
}
<div
ngListbox
aria-label="Amenities_explicit"
orientation="horizontal"
selectionMode="explicit"
multi
>
@for (amenity of amenities; track amenity) {
<div ngOption [value]="amenity" [label]="amenity">
<span class="option-label">{{ amenity }}</span>
</div>
}
</div>
import {Component} from '@angular/core';
import {Listbox, Option} from '@angular/aria/listbox';
@Component({
selector: 'app-root',
templateUrl: './app.html',
styleUrl: './app.css',
imports: [Listbox, Option],
})
export class App {
amenities = ['Washer / Dryer', 'Ramp access', 'Garden', 'Cats OK', 'Dogs OK', 'Smoke-free'];
}
<div
ngListbox
aria-label="Amenities_explicit"
orientation="horizontal"
selectionMode="follow"
multi
>
@for (amenity of amenities; track amenity) {
<div ngOption [value]="amenity" [label]="amenity">
<span class="option-label">{{ amenity }}</span>
</div>
}
</div>
TIP: 下拉清單模式在單選時通常使用 'follow' 模式。
Angular Aria 提供元件測試工具,可用來測試 listbox 元件。
以下範例示範如何在元件測試中使用這些元件測試工具:
import {ComponentFixture, TestBed} from '@angular/core/testing';
import {HarnessLoader} from '@angular/cdk/testing';
import {TestbedHarnessEnvironment} from '@angular/cdk/testing/testbed';
import {ListboxHarness} from '@angular/aria/listbox/testing';
import {MyListboxComponent} from './my-listbox'; // Your component
describe('MyListboxComponent', () => {
let fixture: ComponentFixture<MyListboxComponent>;
let loader: HarnessLoader;
beforeEach(async () => {
TestBed.configureTestingModule({
imports: [MyListboxComponent],
});
fixture = TestBed.createComponent(MyListboxComponent);
await fixture.whenStable();
loader = TestbedHarnessEnvironment.loader(fixture);
});
it('should allow selecting options', async () => {
const listbox = await loader.getHarness(ListboxHarness);
// Verify listbox properties
expect(await listbox.isMulti()).toBe(true);
// Get all options
const options = await listbox.getOptions();
expect(options.length).toBe(2);
// Click an option
await options[0].click();
// Verify option is selected
expect(await options[0].isSelected()).toBe(true);
// Filter options by text
const bananaOption = await listbox.getOptions({text: 'Banana'});
expect(bananaOption.length).toBe(1);
});
});
如需詳細的 API 文件,請查看下列 API 參考:
Listbox 被下列已文件化的下拉清單模式使用:
若需要包含觸發器、彈出視窗與覆蓋層定位的完整下拉清單模式,請參閱那些模式指南,不要只單獨使用 listbox。