آنلاین

Listbox

Overview

directiveای که listای از optionها را برای انتخاب کاربران نمایش می‌دهد و از keyboard navigation، انتخاب single یا multiple و screen reader support پشتیبانی می‌کند.

ts
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',
  ];
}
html
<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>
css
@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;
}
ts
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',
  ];
}
html
<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>
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;
}

.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;
}
ts
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',
  ];
}
html
<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>
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(--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;
}

Usage

Listbox یک directive foundational است که توسط patternهای Select، Multiselect و Autocomplete استفاده می‌شود. برای بیشتر نیازهای dropdown، به جای استفاده مستقیم از listbox، از همان patternهای مستندشده استفاده کنید.

استفاده مستقیم از listbox را در این موارد در نظر بگیرید:

  • ساخت کامپوننت‌های selection سفارشی - ایجاد interfaceهای تخصصی با behavior مشخص.
  • Visible selection listها - نمایش itemهای selectable مستقیم روی صفحه \(نه داخل dropdown\).
  • Custom integration patternها - integration با نیازهای خاص popup یا layout.

از listbox پرهیز کنید وقتی:

  • Navigation menu لازم است - برای actionها و commandها از directive مربوط به Menu استفاده کنید.

قابلیت‌ها

Listbox در Angular یک implementation کاملاً accessible برای list فراهم می‌کند با:

  • Keyboard Navigation - با arrow keyها بین optionها navigate کنید و با Enter یا Space انتخاب کنید.
  • Screen Reader Support - attributeهای ARIA داخلی شامل role="listbox".
  • Single یا Multiple Selection - attribute مربوط به multi mode انتخاب را کنترل می‌کند.
  • Horizontal یا Vertical - attribute مربوط به orientation برای جهت layout.
  • Type-ahead Search - با type کردن characterها به optionهای matching بپرید.
  • Signal-Based Reactivity - مدیریت state reactive با Angular signals.

مثال‌ها

Basic listbox

برنامه‌ها گاهی به listهای selectable نیاز دارند که به جای پنهان شدن در dropdown، مستقیم روی صفحه visible باشند. یک listbox standalone برای این interfaceهای list قابل مشاهده، keyboard navigation و selection فراهم می‌کند.

ts
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',
  ];
}
html
<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>

model signal مربوط به value، two-way binding به itemهای selected را فراهم می‌کند. با selectionMode="explicit"، کاربران Space یا Enter را فشار می‌دهند تا optionها را select کنند. برای patternهای dropdown که listbox را با combobox و overlay positioning ترکیب می‌کنند، pattern مربوط به Select را ببینید.

Horizontal listbox

Listها گاهی به صورت افقی بهتر کار می‌کنند، مثل interfaceهایی شبیه toolbar یا selectionهای tab-style. attribute مربوط به orientation هم layout و هم جهت keyboard navigation را تغییر می‌دهد.

ts
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'];
}
html
<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>
css
@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);
}
ts
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'];
}
html
<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>
css
@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;
}
ts
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'];
}
html
<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>
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(--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"، arrow keyهای چپ و راست به جای بالا و پایین بین optionها navigate می‌کنند. listbox زبان‌های راست‌به‌چپ \(RTL\) را با معکوس کردن جهت navigation به صورت خودکار مدیریت می‌کند.

Selection modeها

Listbox دو selection mode را پشتیبانی می‌کند که کنترل می‌کنند itemها چه زمانی selected شوند.

mode مربوط به 'follow' item دارای focus را به صورت خودکار select می‌کند و وقتی selection زیاد تغییر می‌کند interaction سریع‌تری فراهم می‌کند. mode مربوط به 'explicit' برای confirm کردن selection به Space یا Enter نیاز دارد و هنگام navigation از تغییرات تصادفی جلوگیری می‌کند. patternهای dropdown معمولاً برای single selection از mode مربوط به 'follow' استفاده می‌کنند.

Explicit

ts
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'];
}
html
<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>

Follow

ts
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'];
}
html
<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>
Modeتوضیح
'follow'item دارای focus را به صورت خودکار selected می‌کند و وقتی selection زیاد تغییر می‌کند interaction سریع‌تری فراهم می‌کند
'explicit'برای confirm کردن selection به Space یا Enter نیاز دارد و هنگام navigation از تغییرات تصادفی جلوگیری می‌کند

Testing

Angular Aria برای testing کامپوننت‌های listbox، component harness ارائه می‌کند. این نمونه نحوه استفاده از harnessها را در یک component test نشان می‌دهد:

ts
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 reference

برای مستندات دقیق API، referenceهای API زیر را بررسی کنید:

Patternهای مرتبط

Listbox توسط این patternهای dropdown مستندشده استفاده می‌شود:

  • Select - pattern مربوط به dropdown تک‌انتخابی با readonly combobox + listbox
  • Multiselect - pattern مربوط به dropdown چندانتخابی با readonly combobox + listbox همراه با multi
  • Autocomplete - pattern مربوط به dropdown فیلترپذیر با combobox + listbox

برای patternهای کامل dropdown همراه با trigger، popup و overlay positioning، به جای استفاده تنها از listbox، آن راهنماهای pattern را ببینید.