개요
아코디언은 관련 콘텐츠를 펼치고 접을 수 있는 섹션으로 구성합니다. 페이지 스크롤을 줄이고 사용자가 필요한 정보에 집중하도록 돕습니다. 각 섹션은 트리거 버튼과 콘텐츠 패널로 구성되며, 트리거를 클릭하면 연결된 패널을 표시하거나 숨깁니다.
TS
import {Component} from '@angular/core';
import {
AccordionGroup,
AccordionTrigger,
AccordionPanel,
AccordionContent,
} from '@angular/aria/accordion';
@Component({
selector: 'app-root',
templateUrl: 'app.html',
styleUrl: 'app.css',
imports: [AccordionGroup, AccordionTrigger, AccordionPanel, AccordionContent],
})
export class App {}
HTML
<div ngAccordionGroup class="basic-accordion" [multiExpandable]="false">
<h3>
<span ngAccordionTrigger [panel]="panel1" #trigger1="ngAccordionTrigger" [expanded]="true">
Which attribute tells assistive tech whether the panel is open or closed?
<span
aria-hidden="true"
class="expand-icon"
[class.expand-icon__expanded]="trigger1.expanded()"
></span>
</span>
</h3>
<div ngAccordionPanel #panel1="ngAccordionPanel">
<ng-template ngAccordionContent>
<p>
Use <code>aria-expanded</code> on the button element. Set it to "true" when the content
panel is visible and "false" when the content is hidden. This is a crucial state indicator
for screen reader users.
</p>
</ng-template>
</div>
<h3>
<span ngAccordionTrigger [panel]="panel2" #trigger2="ngAccordionTrigger">
How do you link the button to the content it controls?
<span
aria-hidden="true"
class="expand-icon"
[class.expand-icon__expanded]="trigger2.expanded()"
></span>
</span>
</h3>
<div ngAccordionPanel #panel2="ngAccordionPanel">
<ng-template ngAccordionContent>
<p>
Use the <code>aria-controls</code> attribute on the button, and set its value to match the
id of the related content panel. This establishes a programmatic relationship, allowing
assistive technologies to jump directly to the relevant content.
</p>
</ng-template>
</div>
<h3>
<span ngAccordionTrigger [panel]="panel3" #trigger3="ngAccordionTrigger">
What role should the heading element containing the accordion button have?
<span
aria-hidden="true"
class="expand-icon"
[class.expand-icon__expanded]="trigger3.expanded()"
></span>
</span>
</h3>
<div ngAccordionPanel #panel3="ngAccordionPanel">
<ng-template ngAccordionContent>
<p>
The element containing the button should typically have <code>role="heading"</code> with an
appropriate <code>aria-level</code> to define the structure. This ensures the accordion
section is recognized as a section header, making the page structure navigable for users.
</p>
</ng-template>
</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);
}
[ngAccordionGroup] {
width: 500px;
}
h3 {
font-size: 1rem;
margin: 0;
color: var(--secondary-contrast);
box-sizing: border-box;
position: relative;
}
h3:focus-within::before,
h3:hover::before {
content: '';
position: absolute;
height: 100%;
width: 2px;
background-color: var(--vivid-pink);
top: 0;
left: 0;
}
h3:not(:first-of-type) {
border-block-start: 1px solid var(--senary-contrast);
}
p {
margin: 0;
padding: 0 1.5rem 1.5rem 1.5rem;
color: var(--tertiary-contrast);
font-size: 0.875rem;
}
[ngAccordionTrigger] {
display: flex;
align-items: center;
justify-content: space-between;
outline: none;
cursor: pointer;
padding: 1.5rem;
}
[ngAccordionTrigger][aria-expanded='true'] {
background-image: var(--pink-to-purple-horizontal-gradient);
background-clip: text;
color: transparent;
}
[ngAccordionTrigger][aria-disabled='true'] {
opacity: 0.5;
cursor: default;
}
.expand-icon {
position: relative;
width: 1rem;
height: 1rem;
flex-shrink: 0;
margin-left: 1rem;
}
.expand-icon::before,
.expand-icon::after {
content: '';
position: absolute;
width: 100%;
height: 2px;
top: 50%;
background-color: var(--quaternary-contrast);
transition: .3s ease-out;
}
.expand-icon::after {
transform: rotate(90deg);
}
.expand-icon__expanded::before {
transform: translateY(-50%) rotate(-90deg);
opacity: 0;
}
.expand-icon__expanded::after {
transform: translateY(-50%) rotate(0);
background-color: var(--electric-violet);
}
사용법
아코디언은 콘텐츠를 논리적인 그룹으로 나누고, 사용자가 대체로 한 번에 하나의 섹션을 확인하는 경우에 적합합니다.
아코디언을 사용하기 좋은 경우:
- 여러 질문과 답변으로 구성된 FAQ를 표시할 때
- 긴 폼을 다루기 쉬운 섹션으로 나눌 때
- 콘텐츠가 많은 페이지에서 스크롤을 줄일 때
- 관련 정보를 단계적으로 공개할 때
아코디언을 피해야 하는 경우:
- 탐색 메뉴를 만들 때(메뉴 컴포넌트를 사용하세요)
- 탭 인터페이스를 만들 때(탭 컴포넌트를 사용하세요)
- 접을 수 있는 섹션을 하나만 표시할 때(펼침/접기 패턴을 사용하세요)
- 사용자가 여러 섹션을 동시에 확인해야 할 때(다른 레이아웃을 고려하세요)
주요 기능
- 펼침 모드: 한 번에 하나의 패널만 열거나 여러 패널을 열도록 제어합니다.
- 키보드 탐색: 방향키, Home, End로 트리거 사이를 이동합니다.
- 지연 렌더링: 패널을 펼칠 때만 콘텐츠를 생성해서 초기 로딩 성능을 개선합니다.
- 비활성화 상태: 전체 그룹이나 개별 트리거를 비활성화합니다.
- 포커스 관리: 비활성화된 항목이 키보드 포커스를 받을 수 있는지 제어합니다.
- 코드로 제어: 컴포넌트 코드에서 패널을 펼치거나 접고, 펼침 상태를 전환합니다.
- RTL 지원: 오른쪽에서 왼쪽으로 쓰는 언어를 자동으로 지원합니다.
예제
단일 펼침 모드
[multiExpandable]="false"로 설정하면 한 번에 하나의 패널만 열 수 있습니다.
새 패널을 열면 이전에 열려 있던 패널은 자동으로 닫힙니다.
TS
import {Component} from '@angular/core';
import {
AccordionGroup,
AccordionTrigger,
AccordionPanel,
AccordionContent,
} from '@angular/aria/accordion';
@Component({
selector: 'app-root',
templateUrl: 'app.html',
styleUrl: 'app.css',
imports: [AccordionGroup, AccordionTrigger, AccordionPanel, AccordionContent],
})
export class App {}
HTML
<div ngAccordionGroup class="basic-accordion" [multiExpandable]="false">
<h3>
<span ngAccordionTrigger [panel]="panel1" #trigger1="ngAccordionTrigger" [expanded]="true">
Which attribute tells assistive tech whether the panel is open or closed?
<span
aria-hidden="true"
class="expand-icon"
[class.expand-icon__expanded]="trigger1.expanded()"
></span>
</span>
</h3>
<div ngAccordionPanel #panel1="ngAccordionPanel">
<ng-template ngAccordionContent>
<p>
Use <code>aria-expanded</code> on the button element. Set it to "true" when the content
panel is visible and "false" when the content is hidden. This is a crucial state indicator
for screen reader users.
</p>
</ng-template>
</div>
<h3>
<span ngAccordionTrigger [panel]="panel2" #trigger2="ngAccordionTrigger">
How do you link the button to the content it controls?
<span
aria-hidden="true"
class="expand-icon"
[class.expand-icon__expanded]="trigger2.expanded()"
></span>
</span>
</h3>
<div ngAccordionPanel #panel2="ngAccordionPanel">
<ng-template ngAccordionContent>
<p>
Use the <code>aria-controls</code> attribute on the button, and set its value to match the
id of the related content panel. This establishes a programmatic relationship, allowing
assistive technologies to jump directly to the relevant content.
</p>
</ng-template>
</div>
<h3>
<span ngAccordionTrigger [panel]="panel3" #trigger3="ngAccordionTrigger">
What role should the heading element containing the accordion button have?
<span
aria-hidden="true"
class="expand-icon"
[class.expand-icon__expanded]="trigger3.expanded()"
></span>
</span>
</h3>
<div ngAccordionPanel #panel3="ngAccordionPanel">
<ng-template ngAccordionContent>
<p>
The element containing the button should typically have <code>role="heading"</code> with an
appropriate <code>aria-level</code> to define the structure. This ensures the accordion
section is recognized as a section header, making the page structure navigable for users.
</p>
</ng-template>
</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);
}
[ngAccordionGroup] {
width: 500px;
}
h3 {
font-size: 1rem;
margin: 0;
color: var(--secondary-contrast);
box-sizing: border-box;
position: relative;
}
h3:focus-within::before,
h3:hover::before {
content: '';
position: absolute;
height: 100%;
width: 2px;
background-color: var(--vivid-pink);
top: 0;
left: 0;
}
h3:not(:first-of-type) {
border-block-start: 1px solid var(--senary-contrast);
}
p {
margin: 0;
padding: 0 1.5rem 1.5rem 1.5rem;
color: var(--tertiary-contrast);
font-size: 0.875rem;
}
[ngAccordionTrigger] {
display: flex;
align-items: center;
justify-content: space-between;
outline: none;
cursor: pointer;
padding: 1.5rem;
}
[ngAccordionTrigger][aria-expanded='true'] {
background-image: var(--pink-to-purple-horizontal-gradient);
background-clip: text;
color: transparent;
}
[ngAccordionTrigger][aria-disabled='true'] {
opacity: 0.5;
cursor: default;
}
.expand-icon {
position: relative;
width: 1rem;
height: 1rem;
flex-shrink: 0;
margin-left: 1rem;
}
.expand-icon::before,
.expand-icon::after {
content: '';
position: absolute;
width: 100%;
height: 2px;
top: 50%;
background-color: var(--quaternary-contrast);
transition: .3s ease-out;
}
.expand-icon::after {
transform: rotate(90deg);
}
.expand-icon__expanded::before {
transform: translateY(-50%) rotate(-90deg);
opacity: 0;
}
.expand-icon__expanded::after {
transform: translateY(-50%) rotate(0);
background-color: var(--electric-violet);
}
TS
import {Component} from '@angular/core';
import {
AccordionGroup,
AccordionTrigger,
AccordionPanel,
AccordionContent,
} from '@angular/aria/accordion';
@Component({
selector: 'app-root',
templateUrl: 'app.html',
styleUrl: 'app.css',
imports: [AccordionGroup, AccordionTrigger, AccordionPanel, AccordionContent],
})
export class App {}
HTML
<div ngAccordionGroup class="material-accordion" [multiExpandable]="false">
<h3>
<span ngAccordionTrigger [panel]="panel1" #trigger1="ngAccordionTrigger">
Which attribute tells assistive tech whether the panel is open or closed?
<span
aria-hidden="true"
class="material-symbols-outlined expand-icon"
[class.expand-icon__expanded]="trigger1.expanded()"
translate="no"
>keyboard_arrow_up</span
>
</span>
</h3>
<div ngAccordionPanel #panel1="ngAccordionPanel">
<ng-template ngAccordionContent>
<p>
Use <code>aria-expanded</code> on the button element. Set it to "true" when the content
panel is visible and "false" when the content is hidden. This is a crucial state indicator
for screen reader users.
</p>
</ng-template>
</div>
<h3>
<span ngAccordionTrigger [panel]="panel2" #trigger2="ngAccordionTrigger" [expanded]="true">
How do you link the button to the content it controls?
<span
aria-hidden="true"
class="material-symbols-outlined expand-icon"
[class.expand-icon__expanded]="trigger2.expanded()"
translate="no"
>keyboard_arrow_up</span
>
</span>
</h3>
<div ngAccordionPanel #panel2="ngAccordionPanel">
<ng-template ngAccordionContent>
<p>
Use the <code>aria-controls</code> attribute on the button, and set its value to match the
id of the related content panel. This establishes a programmatic relationship, allowing
assistive technologies to jump directly to the relevant content.
</p>
</ng-template>
</div>
<h3>
<span ngAccordionTrigger [panel]="panel3" #trigger3="ngAccordionTrigger">
What role should the heading element containing the accordion button have?
<span
aria-hidden="true"
class="material-symbols-outlined expand-icon"
[class.expand-icon__expanded]="trigger3.expanded()"
translate="no"
>keyboard_arrow_up</span
>
</span>
</h3>
<div ngAccordionPanel #panel3="ngAccordionPanel">
<ng-template ngAccordionContent>
<p>
The element containing the button should typically have <code>role="heading"</code> with an
appropriate <code>aria-level</code> to define the structure. This ensures the accordion
section is recognized as a section header, making the page structure navigable for users.
</p>
</ng-template>
</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);
}
[ngAccordionGroup] {
width: 500px;
}
h3 {
font-size: 1rem;
margin: 0;
color: var(--secondary-contrast);
box-sizing: border-box;
border-block-start: 1px solid var(--quinary-contrast);
border-inline: 1px solid var(--quinary-contrast);
}
h3:focus-within,
h3:hover {
background-color: var(--septenary-contrast);
}
h3:first-of-type {
border-radius: 1rem 1rem 0 0;
}
h3:last-of-type {
border-block-end: 1px solid var(--quinary-contrast);
border-radius: 0 0 1rem 1rem;
}
h3:last-of-type:has([aria-expanded='true']) {
border-block-end: 0;
border-bottom-left-radius: 0;
border-bottom-right-radius: 0;
}
p {
margin: 0;
padding: 0 1.5rem 1.5rem 1.5rem;
color: var(--tertiary-contrast);
font-size: 0.875rem;
border-inline: 1px solid var(--quinary-contrast);
border-radius: 0 0 1rem 1rem;
border-block-end: 1px solid var(--quinary-contrast);
margin-block-end: 1rem;
}
[ngAccordionTrigger] {
display: flex;
align-items: center;
justify-content: space-between;
outline: none;
cursor: pointer;
padding: 1.5rem;
}
[ngAccordionTrigger][aria-disabled='true'] {
opacity: 0.5;
cursor: default;
}
.expand-icon {
font-size: 1.5rem;
color: var(--quaternary-contrast);
transition: transform .3s ease-out;
}
.expand-icon__expanded {
transform: rotate(180deg);
}
TS
import {Component} from '@angular/core';
import {
AccordionGroup,
AccordionTrigger,
AccordionPanel,
AccordionContent,
} from '@angular/aria/accordion';
@Component({
selector: 'app-root',
templateUrl: 'app.html',
styleUrl: 'app.css',
imports: [AccordionGroup, AccordionTrigger, AccordionPanel, AccordionContent],
})
export class App {}
HTML
<div ngAccordionGroup class="retro-accordion" [multiExpandable]="false">
<h3>
<span ngAccordionTrigger [panel]="panel1" #trigger1="ngAccordionTrigger">
Unlock Treasure Box
<span aria-hidden="true" class="material-symbols-outlined" translate="no">{{
trigger1.expanded() ? 'lock_open_right' : 'lock'
}}</span>
</span>
</h3>
<div ngAccordionPanel #panel1="ngAccordionPanel">
<ng-template ngAccordionContent>
<div class="treasure-box">👻</div>
</ng-template>
</div>
<h3>
<span ngAccordionTrigger [panel]="panel2" #trigger2="ngAccordionTrigger">
Unlock Treasure Box
<span aria-hidden="true" class="material-symbols-outlined" translate="no">{{
trigger2.expanded() ? 'lock_open_right' : 'lock'
}}</span>
</span>
</h3>
<div ngAccordionPanel #panel2="ngAccordionPanel">
<ng-template ngAccordionContent>
<div class="treasure-box">💎💎💎</div>
</ng-template>
</div>
<h3>
<span ngAccordionTrigger [panel]="panel3" #trigger3="ngAccordionTrigger">
Unlock Treasure Box
<span aria-hidden="true" class="material-symbols-outlined" translate="no">{{
trigger3.expanded() ? 'lock_open_right' : 'lock'
}}</span>
</span>
</h3>
<div ngAccordionPanel #panel3="ngAccordionPanel">
<ng-template ngAccordionContent>
<div class="treasure-box">🐸 ribbit...ribbit...</div>
</ng-template>
</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-family: 'Press Start 2P';
--retro-button-color: color-mix(in srgb, var(--symbolic-yellow) 90%, var(--page-background));
--retro-button-text-color: color-mix(in srgb, var(--symbolic-yellow) 10%, white);
--retro-shadow-light: color-mix(in srgb, var(--retro-button-color) 90%, #fff);
--retro-shadow-dark: color-mix(in srgb, var(--retro-button-color) 90%, #000);
--retro-elevated-shadow:
inset 4px 4px 0px 0px var(--retro-shadow-light),
inset -4px -4px 0px 0px var(--retro-shadow-dark), 4px 0px 0px 0px var(--tertiary-contrast),
0px 4px 0px 0px var(--tertiary-contrast), -4px 0px 0px 0px var(--tertiary-contrast),
0px -4px 0px 0px var(--tertiary-contrast);
--retro-flat-shadow:
4px 0px 0px 0px var(--tertiary-contrast), 0px 4px 0px 0px var(--tertiary-contrast),
-4px 0px 0px 0px var(--tertiary-contrast), 0px -4px 0px 0px var(--tertiary-contrast);
--retro-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(--tertiary-contrast),
0px 4px 0px 0px var(--tertiary-contrast), -4px 0px 0px 0px var(--tertiary-contrast),
0px -4px 0px 0px var(--tertiary-contrast), 8px 8px 0px 0px var(--tertiary-contrast);
--retro-pressed-shadow:
inset 4px 4px 0px 0px var(--retro-shadow-dark),
inset -4px -4px 0px 0px var(--retro-shadow-light), 4px 0px 0px 0px var(--tertiary-contrast),
0px 4px 0px 0px var(--tertiary-contrast), -4px 0px 0px 0px var(--tertiary-contrast),
0px -4px 0px 0px var(--tertiary-contrast), 0px 0px 0px 0px var(--tertiary-contrast);
}
[ngAccordionGroup] {
width: 500px;
}
h3 {
font-size: 1rem;
margin: 0;
color: var(--retro-button-text-color);
background-color: var(--retro-button-color);
box-shadow: var(--retro-clickable-shadow);
transition:
transform 0.1s,
box-shadow 0.1s;
}
h3:focus-within,
h3:hover {
box-shadow: var(--retro-pressed-shadow);
transform: translate(1px, 1px);
outline-offset: 6px;
outline: 4px dashed color-mix(in srgb, var(--hot-pink) 60%, transparent);
}
h3:has([aria-disabled='true']) {
opacity: 0.5;
cursor: default;
}
.treasure-box {
margin: 0;
padding: 1rem;
display: flex;
justify-content: center;
align-items: center;
height: 5rem;
background-color: var(--septenary-contrast);
box-shadow: var(--retro-flat-shadow);
}
[ngAccordionTrigger] {
display: flex;
align-items: center;
justify-content: space-between;
outline: none;
cursor: pointer;
padding: 1.5rem;
}
[ngAccordionPanel] {
padding: 1rem;
}
이 모드는 FAQ나 사용자가 한 번에 하나의 답변에 집중하도록 할 때 적합합니다.
다중 펼침 모드
[multiExpandable]="true"로 설정하면 여러 패널을 동시에 열 수 있습니다.
사용자는 다른 패널을 닫지 않고 필요한 만큼 패널을 펼칠 수 있습니다.
TS
import {Component} from '@angular/core';
import {
AccordionGroup,
AccordionTrigger,
AccordionPanel,
AccordionContent,
} from '@angular/aria/accordion';
@Component({
selector: 'app-root',
templateUrl: 'app.html',
styleUrl: 'app.css',
imports: [AccordionGroup, AccordionTrigger, AccordionPanel, AccordionContent],
})
export class App {}
HTML
<div ngAccordionGroup class="basic-accordion">
<h3>
<span ngAccordionTrigger [panel]="panel1" #trigger1="ngAccordionTrigger" [expanded]="true">
Which attribute tells assistive tech whether the panel is open or closed?
<span
aria-hidden="true"
class="expand-icon"
[class.expand-icon__expanded]="trigger1.expanded()"
></span>
</span>
</h3>
<div ngAccordionPanel #panel1="ngAccordionPanel">
<ng-template ngAccordionContent>
<p>
Use <code>aria-expanded</code> on the button element. Set it to "true" when the content
panel is visible and "false" when the content is hidden. This is a crucial state indicator
for screen reader users.
</p>
</ng-template>
</div>
<h3>
<span ngAccordionTrigger [panel]="panel2" #trigger2="ngAccordionTrigger">
How do you link the button to the content it controls?
<span
aria-hidden="true"
class="expand-icon"
[class.expand-icon__expanded]="trigger2.expanded()"
></span>
</span>
</h3>
<div ngAccordionPanel #panel2="ngAccordionPanel">
<ng-template ngAccordionContent>
<p>
Use the <code>aria-controls</code> attribute on the button, and set its value to match the
id of the related content panel. This establishes a programmatic relationship, allowing
assistive technologies to jump directly to the relevant content.
</p>
</ng-template>
</div>
<h3>
<span ngAccordionTrigger [panel]="panel3" #trigger3="ngAccordionTrigger" [expanded]="true">
What role should the heading element containing the accordion button have?
<span
aria-hidden="true"
class="expand-icon"
[class.expand-icon__expanded]="trigger3.expanded()"
></span>
</span>
</h3>
<div ngAccordionPanel #panel3="ngAccordionPanel">
<ng-template ngAccordionContent>
<p>
The element containing the button should typically have <code>role="heading"</code> with an
appropriate <code>aria-level</code> to define the structure. This ensures the accordion
section is recognized as a section header, making the page structure navigable for users.
</p>
</ng-template>
</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);
}
[ngAccordionGroup] {
width: 500px;
}
h3 {
font-size: 1rem;
margin: 0;
color: var(--secondary-contrast);
box-sizing: border-box;
position: relative;
}
h3:focus-within::before,
h3:hover::before {
content: '';
position: absolute;
height: 100%;
width: 2px;
background-color: var(--vivid-pink);
top: 0;
left: 0;
}
h3:not(:first-of-type) {
border-block-start: 1px solid var(--senary-contrast);
}
p {
margin: 0;
padding: 0 1.5rem 1.5rem 1.5rem;
color: var(--tertiary-contrast);
font-size: 0.875rem;
}
[ngAccordionTrigger] {
display: flex;
align-items: center;
justify-content: space-between;
outline: none;
cursor: pointer;
padding: 1.5rem;
}
[ngAccordionTrigger][aria-expanded='true'] {
background-image: var(--pink-to-purple-horizontal-gradient);
background-clip: text;
color: transparent;
}
[ngAccordionTrigger][aria-disabled='true'] {
opacity: 0.5;
cursor: default;
}
.expand-icon {
position: relative;
width: 1rem;
height: 1rem;
flex-shrink: 0;
margin-left: 1rem;
}
.expand-icon::before,
.expand-icon::after {
content: '';
position: absolute;
width: 100%;
height: 2px;
top: 50%;
background-color: var(--quaternary-contrast);
transition: .3s ease-out;
}
.expand-icon::after {
transform: rotate(90deg);
}
.expand-icon__expanded::before {
transform: translateY(-50%) rotate(-90deg);
opacity: 0;
}
.expand-icon__expanded::after {
transform: translateY(-50%) rotate(0);
background-color: var(--electric-violet);
}
TS
import {Component} from '@angular/core';
import {
AccordionGroup,
AccordionTrigger,
AccordionPanel,
AccordionContent,
} from '@angular/aria/accordion';
@Component({
selector: 'app-root',
templateUrl: 'app.html',
styleUrl: 'app.css',
imports: [AccordionGroup, AccordionTrigger, AccordionPanel, AccordionContent],
})
export class App {}
HTML
<div ngAccordionGroup class="material-accordion">
<h3>
<span ngAccordionTrigger [panel]="panel1" #trigger1="ngAccordionTrigger" [expanded]="true">
Which attribute tells assistive tech whether the panel is open or closed?
<span
aria-hidden="true"
class="material-symbols-outlined expand-icon"
[class.expand-icon__expanded]="trigger1.expanded()"
translate="no"
>keyboard_arrow_up</span
>
</span>
</h3>
<div ngAccordionPanel #panel1="ngAccordionPanel">
<ng-template ngAccordionContent>
<p>
Use <code>aria-expanded</code> on the button element. Set it to "true" when the content
panel is visible and "false" when the content is hidden. This is a crucial state indicator
for screen reader users.
</p>
</ng-template>
</div>
<h3>
<span ngAccordionTrigger [panel]="panel2" #trigger2="ngAccordionTrigger">
How do you link the button to the content it controls?
<span
aria-hidden="true"
class="material-symbols-outlined expand-icon"
[class.expand-icon__expanded]="trigger2.expanded()"
translate="no"
>keyboard_arrow_up</span
>
</span>
</h3>
<div ngAccordionPanel #panel2="ngAccordionPanel">
<ng-template ngAccordionContent>
<p>
Use the <code>aria-controls</code> attribute on the button, and set its value to match the
id of the related content panel. This establishes a programmatic relationship, allowing
assistive technologies to jump directly to the relevant content.
</p>
</ng-template>
</div>
<h3>
<span ngAccordionTrigger [panel]="panel3" #trigger3="ngAccordionTrigger" [expanded]="true">
What role should the heading element containing the accordion button have?
<span
aria-hidden="true"
class="material-symbols-outlined expand-icon"
[class.expand-icon__expanded]="trigger3.expanded()"
translate="no"
>keyboard_arrow_up</span
>
</span>
</h3>
<div ngAccordionPanel #panel3="ngAccordionPanel">
<ng-template ngAccordionContent>
<p>
The element containing the button should typically have <code>role="heading"</code> with an
appropriate <code>aria-level</code> to define the structure. This ensures the accordion
section is recognized as a section header, making the page structure navigable for users.
</p>
</ng-template>
</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);
}
[ngAccordionGroup] {
width: 500px;
}
h3 {
font-size: 1rem;
margin: 0;
color: var(--secondary-contrast);
box-sizing: border-box;
border-block-start: 1px solid var(--quinary-contrast);
border-inline: 1px solid var(--quinary-contrast);
}
h3:focus-within,
h3:hover {
background-color: var(--septenary-contrast);
}
h3:first-of-type {
border-radius: 1rem 1rem 0 0;
}
h3:last-of-type {
border-block-end: 1px solid var(--quinary-contrast);
border-radius: 0 0 1rem 1rem;
}
h3:last-of-type:has([aria-expanded='true']) {
border-block-end: 0;
border-bottom-left-radius: 0;
border-bottom-right-radius: 0;
}
p {
margin: 0;
padding: 0 1.5rem 1.5rem 1.5rem;
color: var(--tertiary-contrast);
font-size: 0.875rem;
border-inline: 1px solid var(--quinary-contrast);
border-radius: 0 0 1rem 1rem;
border-block-end: 1px solid var(--quinary-contrast);
margin-block-end: 1rem;
}
[ngAccordionTrigger] {
display: flex;
align-items: center;
justify-content: space-between;
outline: none;
cursor: pointer;
padding: 1.5rem;
}
[ngAccordionTrigger][aria-disabled='true'] {
opacity: 0.5;
cursor: default;
}
.expand-icon {
font-size: 1.5rem;
color: var(--quaternary-contrast);
transition: transform .3s ease-out;
}
.expand-icon__expanded {
transform: rotate(180deg);
}
TS
import {Component} from '@angular/core';
import {
AccordionGroup,
AccordionTrigger,
AccordionPanel,
AccordionContent,
} from '@angular/aria/accordion';
@Component({
selector: 'app-root',
templateUrl: 'app.html',
styleUrl: 'app.css',
imports: [AccordionGroup, AccordionTrigger, AccordionPanel, AccordionContent],
})
export class App {}
HTML
<div ngAccordionGroup class="retro-accordion">
<h3>
<span ngAccordionTrigger [panel]="panel1" #trigger1="ngAccordionTrigger">
Unlock Treasure Box
<span aria-hidden="true" class="material-symbols-outlined" translate="no">{{
trigger1.expanded() ? 'lock_open_right' : 'lock'
}}</span>
</span>
</h3>
<div ngAccordionPanel #panel1="ngAccordionPanel">
<ng-template ngAccordionContent>
<div class="treasure-box">👻</div>
</ng-template>
</div>
<h3>
<span ngAccordionTrigger [panel]="panel2" #trigger2="ngAccordionTrigger">
Unlock Treasure Box
<span aria-hidden="true" class="material-symbols-outlined" translate="no">{{
trigger2.expanded() ? 'lock_open_right' : 'lock'
}}</span>
</span>
</h3>
<div ngAccordionPanel #panel2="ngAccordionPanel">
<ng-template ngAccordionContent>
<div class="treasure-box">💎💎💎</div>
</ng-template>
</div>
<h3>
<span ngAccordionTrigger [panel]="panel3" #trigger3="ngAccordionTrigger">
Unlock Treasure Box
<span aria-hidden="true" class="material-symbols-outlined" translate="no">{{
trigger3.expanded() ? 'lock_open_right' : 'lock'
}}</span>
</span>
</h3>
<div ngAccordionPanel #panel3="ngAccordionPanel">
<ng-template ngAccordionContent>
<div class="treasure-box">🐸 ribbit...ribbit...</div>
</ng-template>
</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-family: 'Press Start 2P';
--retro-button-color: color-mix(in srgb, var(--symbolic-yellow) 90%, var(--page-background));
--retro-button-text-color: color-mix(in srgb, var(--symbolic-yellow) 10%, white);
--retro-shadow-light: color-mix(in srgb, var(--retro-button-color) 90%, #fff);
--retro-shadow-dark: color-mix(in srgb, var(--retro-button-color) 90%, #000);
--retro-elevated-shadow:
inset 4px 4px 0px 0px var(--retro-shadow-light),
inset -4px -4px 0px 0px var(--retro-shadow-dark), 4px 0px 0px 0px var(--tertiary-contrast),
0px 4px 0px 0px var(--tertiary-contrast), -4px 0px 0px 0px var(--tertiary-contrast),
0px -4px 0px 0px var(--tertiary-contrast);
--retro-flat-shadow:
4px 0px 0px 0px var(--tertiary-contrast), 0px 4px 0px 0px var(--tertiary-contrast),
-4px 0px 0px 0px var(--tertiary-contrast), 0px -4px 0px 0px var(--tertiary-contrast);
--retro-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(--tertiary-contrast),
0px 4px 0px 0px var(--tertiary-contrast), -4px 0px 0px 0px var(--tertiary-contrast),
0px -4px 0px 0px var(--tertiary-contrast), 8px 8px 0px 0px var(--tertiary-contrast);
--retro-pressed-shadow:
inset 4px 4px 0px 0px var(--retro-shadow-dark),
inset -4px -4px 0px 0px var(--retro-shadow-light), 4px 0px 0px 0px var(--tertiary-contrast),
0px 4px 0px 0px var(--tertiary-contrast), -4px 0px 0px 0px var(--tertiary-contrast),
0px -4px 0px 0px var(--tertiary-contrast), 0px 0px 0px 0px var(--tertiary-contrast);
}
[ngAccordionGroup] {
width: 500px;
}
h3 {
font-size: 1rem;
margin: 0;
color: var(--retro-button-text-color);
background-color: var(--retro-button-color);
box-shadow: var(--retro-clickable-shadow);
transition:
transform 0.1s,
box-shadow 0.1s;
}
h3:focus-within,
h3:hover {
box-shadow: var(--retro-pressed-shadow);
transform: translate(1px, 1px);
outline-offset: 6px;
outline: 4px dashed color-mix(in srgb, var(--hot-pink) 60%, transparent);
}
h3:has([aria-disabled='true']) {
opacity: 0.5;
cursor: default;
}
.treasure-box {
margin: 0;
padding: 1rem;
display: flex;
justify-content: center;
align-items: center;
height: 5rem;
background-color: var(--septenary-contrast);
box-shadow: var(--retro-flat-shadow);
}
[ngAccordionTrigger] {
display: flex;
align-items: center;
justify-content: space-between;
outline: none;
cursor: pointer;
padding: 1.5rem;
}
[ngAccordionPanel] {
padding: 1rem;
}
이 모드는 폼의 섹션이나 여러 패널의 콘텐츠를 비교해야 하는 경우에 유용합니다.
NOTE: multiExpandable 입력 프로퍼티의 기본값은 true입니다.
한 번에 하나의 패널만 펼치려면 명시적으로 false로 설정하세요.
비활성화된 아코디언 항목
disabled 입력 프로퍼티로 특정 트리거를 비활성화할 수 있습니다.
키보드 탐색 중 비활성화된 항목의 동작은 아코디언 그룹의 softDisabled 입력 프로퍼티로 제어합니다.
TS
import {Component} from '@angular/core';
import {
AccordionGroup,
AccordionTrigger,
AccordionPanel,
AccordionContent,
} from '@angular/aria/accordion';
@Component({
selector: 'app-root',
templateUrl: 'app.html',
styleUrl: 'app.css',
imports: [AccordionGroup, AccordionTrigger, AccordionPanel, AccordionContent],
})
export class App {}
HTML
<div ngAccordionGroup class="basic-accordion">
<h3>
<span ngAccordionTrigger [panel]="panel1" #trigger1="ngAccordionTrigger" [expanded]="true">
Which attribute tells assistive tech whether the panel is open or closed?
<span
aria-hidden="true"
class="expand-icon"
[class.expand-icon__expanded]="trigger1.expanded()"
></span>
</span>
</h3>
<div ngAccordionPanel #panel1="ngAccordionPanel">
<ng-template ngAccordionContent>
<p>
Use <code>aria-expanded</code> on the button element. Set it to "true" when the content
panel is visible and "false" when the content is hidden. This is a crucial state indicator
for screen reader users.
</p>
</ng-template>
</div>
<h3>
<span ngAccordionTrigger [panel]="panel2" #trigger2="ngAccordionTrigger" disabled>
How do you link the button to the content it controls?
<span
aria-hidden="true"
class="expand-icon"
[class.expand-icon__expanded]="trigger2.expanded()"
></span>
</span>
</h3>
<div ngAccordionPanel #panel2="ngAccordionPanel">
<ng-template ngAccordionContent>
<p>
Use the <code>aria-controls</code> attribute on the button, and set its value to match the
id of the related content panel. This establishes a programmatic relationship, allowing
assistive technologies to jump directly to the relevant content.
</p>
</ng-template>
</div>
<h3>
<span ngAccordionTrigger [panel]="panel3" #trigger3="ngAccordionTrigger">
What role should the heading element containing the accordion button have?
<span
aria-hidden="true"
class="expand-icon"
[class.expand-icon__expanded]="trigger3.expanded()"
></span>
</span>
</h3>
<div ngAccordionPanel panelId="q3" #panel3="ngAccordionPanel">
<ng-template ngAccordionContent>
<p>
The element containing the button should typically have <code>role="heading"</code> with an
appropriate <code>aria-level</code> to define the structure. This ensures the accordion
section is recognized as a section header, making the page structure navigable for users.
</p>
</ng-template>
</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);
}
[ngAccordionGroup] {
width: 500px;
}
h3 {
font-size: 1rem;
margin: 0;
color: var(--secondary-contrast);
box-sizing: border-box;
position: relative;
}
h3:focus-within::before,
h3:hover::before {
content: '';
position: absolute;
height: 100%;
width: 2px;
background-color: var(--vivid-pink);
top: 0;
left: 0;
}
h3:not(:first-of-type) {
border-block-start: 1px solid var(--senary-contrast);
}
p {
margin: 0;
padding: 0 1.5rem 1.5rem 1.5rem;
color: var(--tertiary-contrast);
font-size: 0.875rem;
}
[ngAccordionTrigger] {
display: flex;
align-items: center;
justify-content: space-between;
outline: none;
cursor: pointer;
padding: 1.5rem;
}
[ngAccordionTrigger][aria-expanded='true'] {
background-image: var(--pink-to-purple-horizontal-gradient);
background-clip: text;
color: transparent;
}
[ngAccordionTrigger][aria-disabled='true'] {
opacity: 0.5;
cursor: default;
}
.expand-icon {
position: relative;
width: 1rem;
height: 1rem;
flex-shrink: 0;
margin-left: 1rem;
}
.expand-icon::before,
.expand-icon::after {
content: '';
position: absolute;
width: 100%;
height: 2px;
top: 50%;
background-color: var(--quaternary-contrast);
transition: .3s ease-out;
}
.expand-icon::after {
transform: rotate(90deg);
}
.expand-icon__expanded::before {
transform: translateY(-50%) rotate(-90deg);
opacity: 0;
}
.expand-icon__expanded::after {
transform: translateY(-50%) rotate(0);
background-color: var(--electric-violet);
}
TS
import {Component} from '@angular/core';
import {
AccordionGroup,
AccordionTrigger,
AccordionPanel,
AccordionContent,
} from '@angular/aria/accordion';
@Component({
selector: 'app-root',
templateUrl: 'app.html',
styleUrl: 'app.css',
imports: [AccordionGroup, AccordionTrigger, AccordionPanel, AccordionContent],
})
export class App {}
HTML
<div ngAccordionGroup class="material-accordion">
<h3>
<span ngAccordionTrigger [panel]="panel1" #trigger1="ngAccordionTrigger" [expanded]="true">
Which attribute tells assistive tech whether the panel is open or closed?
<span
aria-hidden="true"
class="material-symbols-outlined expand-icon"
[class.expand-icon__expanded]="trigger1.expanded()"
translate="no"
>keyboard_arrow_up</span
>
</span>
</h3>
<div ngAccordionPanel #panel1="ngAccordionPanel">
<ng-template ngAccordionContent>
<p>
Use <code>aria-expanded</code> on the button element. Set it to "true" when the content
panel is visible and "false" when the content is hidden. This is a crucial state indicator
for screen reader users.
</p>
</ng-template>
</div>
<h3>
<span ngAccordionTrigger [panel]="panel2" #trigger2="ngAccordionTrigger" disabled>
How do you link the button to the content it controls?
<span
aria-hidden="true"
class="material-symbols-outlined expand-icon"
[class.expand-icon__expanded]="trigger2.expanded()"
translate="no"
>keyboard_arrow_up</span
>
</span>
</h3>
<div ngAccordionPanel #panel2="ngAccordionPanel">
<ng-template ngAccordionContent>
<p>
Use the <code>aria-controls</code> attribute on the button, and set its value to match the
id of the related content panel. This establishes a programmatic relationship, allowing
assistive technologies to jump directly to the relevant content.
</p>
</ng-template>
</div>
<h3>
<span ngAccordionTrigger [panel]="panel3" #trigger3="ngAccordionTrigger">
What role should the heading element containing the accordion button have?
<span
aria-hidden="true"
class="material-symbols-outlined expand-icon"
[class.expand-icon__expanded]="trigger3.expanded()"
translate="no"
>keyboard_arrow_up</span
>
</span>
</h3>
<div ngAccordionPanel #panel3="ngAccordionPanel">
<ng-template ngAccordionContent>
<p>
The element containing the button should typically have <code>role="heading"</code> with an
appropriate <code>aria-level</code> to define the structure. This ensures the accordion
section is recognized as a section header, making the page structure navigable for users.
</p>
</ng-template>
</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);
}
[ngAccordionGroup] {
width: 500px;
}
h3 {
font-size: 1rem;
margin: 0;
color: var(--secondary-contrast);
box-sizing: border-box;
border-block-start: 1px solid var(--quinary-contrast);
border-inline: 1px solid var(--quinary-contrast);
}
h3:focus-within,
h3:hover {
background-color: var(--septenary-contrast);
}
h3:first-of-type {
border-radius: 1rem 1rem 0 0;
}
h3:last-of-type {
border-block-end: 1px solid var(--quinary-contrast);
border-radius: 0 0 1rem 1rem;
}
h3:last-of-type:has([aria-expanded='true']) {
border-block-end: 0;
border-bottom-left-radius: 0;
border-bottom-right-radius: 0;
}
p {
margin: 0;
padding: 0 1.5rem 1.5rem 1.5rem;
color: var(--tertiary-contrast);
font-size: 0.875rem;
border-inline: 1px solid var(--quinary-contrast);
border-radius: 0 0 1rem 1rem;
border-block-end: 1px solid var(--quinary-contrast);
margin-block-end: 1rem;
}
[ngAccordionTrigger] {
display: flex;
align-items: center;
justify-content: space-between;
outline: none;
cursor: pointer;
padding: 1.5rem;
}
[ngAccordionTrigger][aria-disabled='true'] {
opacity: 0.5;
cursor: default;
}
.expand-icon {
font-size: 1.5rem;
color: var(--quaternary-contrast);
transition: transform .3s ease-out;
}
.expand-icon__expanded {
transform: rotate(180deg);
}
TS
import {Component} from '@angular/core';
import {
AccordionGroup,
AccordionTrigger,
AccordionPanel,
AccordionContent,
} from '@angular/aria/accordion';
@Component({
selector: 'app-root',
templateUrl: 'app.html',
styleUrl: 'app.css',
imports: [AccordionGroup, AccordionTrigger, AccordionPanel, AccordionContent],
})
export class App {}
HTML
<div ngAccordionGroup class="retro-accordion">
<h3>
<span ngAccordionTrigger [panel]="panel1" #trigger1="ngAccordionTrigger">
Unlock Treasure Box
<span aria-hidden="true" class="material-symbols-outlined" translate="no">{{
trigger1.expanded() ? 'lock_open_right' : 'lock'
}}</span>
</span>
</h3>
<div ngAccordionPanel #panel1="ngAccordionPanel">
<ng-template ngAccordionContent>
<div class="treasure-box">👻</div>
</ng-template>
</div>
<h3>
<span ngAccordionTrigger [panel]="panel2" #trigger2="ngAccordionTrigger" disabled>
Unlock Treasure Box
<span aria-hidden="true" class="material-symbols-outlined" translate="no">{{
trigger2.expanded() ? 'lock_open_right' : 'lock'
}}</span>
</span>
</h3>
<div ngAccordionPanel #panel2="ngAccordionPanel">
<ng-template ngAccordionContent>
<div class="treasure-box">💎💎💎</div>
</ng-template>
</div>
<h3>
<span ngAccordionTrigger [panel]="panel3" #trigger3="ngAccordionTrigger" [expanded]="true">
Unlock Treasure Box
<span aria-hidden="true" class="material-symbols-outlined" translate="no">{{
trigger3.expanded() ? 'lock_open_right' : 'lock'
}}</span>
</span>
</h3>
<div ngAccordionPanel #panel3="ngAccordionPanel">
<ng-template ngAccordionContent>
<div class="treasure-box">🐸 ribbit...ribbit...</div>
</ng-template>
</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-family: 'Press Start 2P';
--retro-button-color: color-mix(in srgb, var(--symbolic-yellow) 90%, var(--page-background));
--retro-button-text-color: color-mix(in srgb, var(--symbolic-yellow) 10%, white);
--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(--quaternary-contrast),
0px 4px 0px 0px var(--quaternary-contrast), -4px 0px 0px 0px var(--quaternary-contrast),
0px -4px 0px 0px var(--quaternary-contrast);
--retro-flat-shadow:
4px 0px 0px 0px var(--quaternary-contrast), 0px 4px 0px 0px var(--quaternary-contrast),
-4px 0px 0px 0px var(--quaternary-contrast), 0px -4px 0px 0px var(--quaternary-contrast);
--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(--quaternary-contrast),
0px 4px 0px 0px var(--quaternary-contrast), -4px 0px 0px 0px var(--quaternary-contrast),
0px -4px 0px 0px var(--quaternary-contrast), 8px 8px 0px 0px var(--quaternary-contrast);
--retro-pressed-shadow:
inset 4px 4px 0px 0px var(--retro-shadow-dark),
inset -4px -4px 0px 0px var(--retro-shadow-light), 4px 0px 0px 0px var(--quaternary-contrast),
0px 4px 0px 0px var(--quaternary-contrast), -4px 0px 0px 0px var(--quaternary-contrast),
0px -4px 0px 0px var(--quaternary-contrast), 0px 0px 0px 0px var(--quaternary-contrast);
}
[ngAccordionGroup] {
width: 500px;
}
h3 {
font-size: 1rem;
margin: 0;
color: var(--retro-button-text-color);
background-color: var(--retro-button-color);
box-shadow: var(--retro-clickable-shadow);
transition:
transform 0.1s,
box-shadow 0.1s;
}
h3:focus-within,
h3:hover {
box-shadow: var(--retro-pressed-shadow);
transform: translate(1px, 1px);
outline-offset: 6px;
outline: 4px dashed color-mix(in srgb, var(--hot-pink) 60%, transparent);
}
h3:has([aria-disabled='true']) {
opacity: 0.5;
cursor: default;
}
.treasure-box {
margin: 0;
padding: 1rem;
display: flex;
justify-content: center;
align-items: center;
height: 5rem;
background-color: var(--septenary-contrast);
box-shadow: var(--retro-flat-shadow);
}
[ngAccordionTrigger] {
display: flex;
align-items: center;
justify-content: space-between;
outline: none;
cursor: pointer;
padding: 1.5rem;
}
[ngAccordionPanel] {
padding: 1rem;
}
[softDisabled]="true"(기본값)이면 비활성화된 항목도 포커스를 받을 수 있지만 활성화할 수는 없습니다.
[softDisabled]="false"이면 키보드 탐색에서 비활성화된 항목을 완전히 건너뜁니다.
콘텐츠 지연 렌더링
ng-template에 ngAccordionContent 디렉티브를 적용하면 패널을 펼칠 때까지 콘텐츠 렌더링을 미룰 수 있습니다.
이미지, 차트, 복잡한 컴포넌트처럼 처리 비용이 큰 콘텐츠를 포함한 아코디언의 성능을 개선할 수 있습니다.
<div ngAccordionGroup>
<div>
<button ngAccordionTrigger [panel]="panel1">Trigger Text</button>
<div ngAccordionPanel #panel1="ngAccordionPanel">
<ng-template ngAccordionContent>
<!-- 이 콘텐츠는 패널을 열 때만 렌더링됩니다. -->
<img src="large-image.jpg" alt="Description" />
<app-expensive-component />
</ng-template>
</div>
</div>
</div>
기본적으로 패널을 접으면 콘텐츠가 DOM에서 제거됩니다.
예를 들어 패널을 접어도 초기화되지 않아야 하는 동영상이 있다면, [preserveContent]="true"로 설정해서 콘텐츠를 유지하세요.
테스트
Angular Aria는 아코디언 컴포넌트를 테스트할 수 있는 컴포넌트 하네스를 제공합니다. 컴포넌트 테스트에서 하네스를 사용하는 예제는 다음과 같습니다.
import {ComponentFixture, TestBed} from '@angular/core/testing';
import {HarnessLoader} from '@angular/cdk/testing';
import {TestbedHarnessEnvironment} from '@angular/cdk/testing/testbed';
import {AccordionGroupHarness} from '@angular/aria/accordion/testing';
import {MyAccordionComponent} from './my-accordion'; // 테스트할 컴포넌트
describe('MyAccordionComponent', () => {
let fixture: ComponentFixture<MyAccordionComponent>;
let loader: HarnessLoader;
beforeEach(async () => {
TestBed.configureTestingModule({
imports: [MyAccordionComponent],
});
fixture = TestBed.createComponent(MyAccordionComponent);
await fixture.whenStable();
loader = TestbedHarnessEnvironment.loader(fixture);
});
it('should allow expanding panels', async () => {
// 아코디언 그룹 하네스를 불러옵니다.
const group = await loader.getHarness(AccordionGroupHarness);
// 그룹에 있는 모든 개별 아코디언(항목)을 가져옵니다.
const accordions = await group.getAccordions();
expect(accordions.length).toBe(3);
// 초기 상태를 확인합니다(첫 번째 패널은 펼쳐지고 나머지는 접힌 상태).
expect(await accordions[0].isExpanded()).toBe(true);
expect(await accordions[1].isExpanded()).toBe(false);
// 두 번째 패널을 펼칩니다.
await accordions[1].expand();
// 변경된 상태를 확인합니다.
expect(await accordions[1].isExpanded()).toBe(true);
// multiExpandable이 false이면 이제 첫 번째 패널은 접혀 있어야 합니다.
expect(await accordions[0].isExpanded()).toBe(false);
});
});
API 레퍼런스
자세한 API 문서는 다음 API 레퍼런스를 참고하세요.