ngx-opalbytes-components 1.26.0 → 1.28.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +370 -1
- package/fesm2022/ngx-opalbytes-components.mjs +562 -3
- package/fesm2022/ngx-opalbytes-components.mjs.map +1 -1
- package/package.json +1 -1
- package/src/lib/shared/components/accordion/accordion.component.css +30 -0
- package/src/lib/shared/components/accordion/accordion.component.html +1 -0
- package/src/lib/shared/components/accordion/accordion.component.spec.ts +117 -0
- package/src/lib/shared/components/accordion/accordion.component.ts +195 -0
- package/src/lib/shared/components/accordion/accordion.token.ts +40 -0
- package/src/lib/shared/components/accordion-item/accordion-item.component.css +137 -0
- package/src/lib/shared/components/accordion-item/accordion-item.component.html +52 -0
- package/src/lib/shared/components/accordion-item/accordion-item.component.ts +157 -0
- package/src/lib/shared/components/accordion-item/accordion-item.spec.ts +22 -0
- package/src/lib/shared/components/checkbox/checkbox.component.css +131 -0
- package/src/lib/shared/components/checkbox/checkbox.component.html +84 -0
- package/src/lib/shared/components/checkbox/checkbox.component.spec.ts +344 -0
- package/src/lib/shared/components/checkbox/checkbox.component.ts +258 -0
- package/src/lib/shared/components/checkbox/checkbox.stories.ts +240 -0
- package/src/lib/shared/components/index.ts +4 -0
- package/types/ngx-opalbytes-components.d.ts +341 -126
package/package.json
CHANGED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
:host {
|
|
2
|
+
display: flex;
|
|
3
|
+
flex-direction: column;
|
|
4
|
+
box-sizing: border-box;
|
|
5
|
+
width: 100%;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
:host(.cao-accordion--hidden) {
|
|
9
|
+
display: none;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/* Painéis unidos: o fundo do host aparece nos 1px de gap e funciona como divisória. */
|
|
13
|
+
:host(.cao-accordion--default) {
|
|
14
|
+
gap: 1px;
|
|
15
|
+
background: var(--cao-accordion-border-color, #e5e7eb);
|
|
16
|
+
border: 1px solid var(--cao-accordion-border-color, #e5e7eb);
|
|
17
|
+
border-radius: var(--cao-accordion-radius, 8px);
|
|
18
|
+
overflow: hidden;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
:host(.cao-accordion--flat) {
|
|
22
|
+
gap: 1px;
|
|
23
|
+
background: var(--cao-accordion-border-color, #e5e7eb);
|
|
24
|
+
border-top: 1px solid var(--cao-accordion-border-color, #e5e7eb);
|
|
25
|
+
border-bottom: 1px solid var(--cao-accordion-border-color, #e5e7eb);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
:host(.cao-accordion--separated) {
|
|
29
|
+
gap: var(--cao-accordion-gap, 8px);
|
|
30
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<ng-content />
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { Component, input, viewChild } from '@angular/core';
|
|
2
|
+
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
|
3
|
+
|
|
4
|
+
import { CaoAccordionComponent } from './accordion.component';
|
|
5
|
+
import { CaoAccordionItemComponent } from '../accordion-item/accordion-item.component';
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@Component({
|
|
9
|
+
imports: [CaoAccordionItemComponent, CaoAccordionComponent],
|
|
10
|
+
template: `
|
|
11
|
+
<cao-accordion dataCy="acc" headerBgColor="#111111" [multi]="multi()">
|
|
12
|
+
<cao-accordion-item dataCy="one" label="Um">A</cao-accordion-item>
|
|
13
|
+
<cao-accordion-item dataCy="two" label="Dois">B</cao-accordion-item>
|
|
14
|
+
<cao-accordion-item dataCy="dis" label="Desabilitado" [isDisabled]="true">C</cao-accordion-item>
|
|
15
|
+
<cao-accordion-item dataCy="hid" label="Oculto" [isVisible]="false">D</cao-accordion-item>
|
|
16
|
+
</cao-accordion>
|
|
17
|
+
`,
|
|
18
|
+
})
|
|
19
|
+
class HostComponent {
|
|
20
|
+
readonly multi = input<boolean>(false);
|
|
21
|
+
readonly accordion = viewChild.required(CaoAccordionComponent);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
describe('CaoAccordion', () => {
|
|
25
|
+
let fixture: ComponentFixture<HostComponent>;
|
|
26
|
+
let host: HTMLElement;
|
|
27
|
+
|
|
28
|
+
const header = (cy: string): HTMLButtonElement =>
|
|
29
|
+
host.querySelector<HTMLButtonElement>(`[data-cy="${cy}-header"]`) as HTMLButtonElement;
|
|
30
|
+
|
|
31
|
+
const click = async (cy: string): Promise<void> => {
|
|
32
|
+
header(cy).click();
|
|
33
|
+
fixture.detectChanges();
|
|
34
|
+
await fixture.whenStable();
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
beforeEach(async () => {
|
|
38
|
+
await TestBed.configureTestingModule({ imports: [HostComponent] }).compileComponents();
|
|
39
|
+
fixture = TestBed.createComponent(HostComponent);
|
|
40
|
+
host = fixture.nativeElement as HTMLElement;
|
|
41
|
+
fixture.detectChanges();
|
|
42
|
+
await fixture.whenStable();
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('expõe data-cy no accordion, no item, no cabeçalho e no conteúdo', () => {
|
|
46
|
+
expect(host.querySelector('[data-cy="acc"]')).not.toBeNull();
|
|
47
|
+
expect(host.querySelector('[data-cy="one"]')).not.toBeNull();
|
|
48
|
+
expect(host.querySelector('[data-cy="one-header"]')).not.toBeNull();
|
|
49
|
+
expect(host.querySelector('[data-cy="one-content"]')).not.toBeNull();
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('expande e recolhe ao clicar no cabeçalho', async () => {
|
|
53
|
+
expect(header('one').getAttribute('aria-expanded')).toBe('false');
|
|
54
|
+
await click('one');
|
|
55
|
+
expect(header('one').getAttribute('aria-expanded')).toBe('true');
|
|
56
|
+
await click('one');
|
|
57
|
+
expect(header('one').getAttribute('aria-expanded')).toBe('false');
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it('no modo único, expandir um item recolhe os demais', async () => {
|
|
61
|
+
await click('one');
|
|
62
|
+
await click('two');
|
|
63
|
+
expect(header('one').getAttribute('aria-expanded')).toBe('false');
|
|
64
|
+
expect(header('two').getAttribute('aria-expanded')).toBe('true');
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it('no modo multi, mantém vários itens expandidos', async () => {
|
|
68
|
+
fixture.componentRef.setInput('multi', true);
|
|
69
|
+
fixture.detectChanges();
|
|
70
|
+
await fixture.whenStable();
|
|
71
|
+
|
|
72
|
+
await click('one');
|
|
73
|
+
await click('two');
|
|
74
|
+
expect(header('one').getAttribute('aria-expanded')).toBe('true');
|
|
75
|
+
expect(header('two').getAttribute('aria-expanded')).toBe('true');
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it('não alterna item desabilitado', async () => {
|
|
79
|
+
expect(header('dis').disabled).toBe(true);
|
|
80
|
+
await click('dis');
|
|
81
|
+
expect(header('dis').getAttribute('aria-expanded')).toBe('false');
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it('oculta o item quando isVisible é false', () => {
|
|
85
|
+
const item = host.querySelector('[data-cy="hid"]') as HTMLElement;
|
|
86
|
+
expect(item.classList.contains('cao-accordion-item--hidden')).toBe(true);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it('openAll/closeAll funcionam apenas conforme o modo', async () => {
|
|
90
|
+
const accordion = fixture.componentInstance.accordion();
|
|
91
|
+
accordion.openAll();
|
|
92
|
+
fixture.detectChanges();
|
|
93
|
+
await fixture.whenStable();
|
|
94
|
+
expect(header('one').getAttribute('aria-expanded')).toBe('false');
|
|
95
|
+
|
|
96
|
+
fixture.componentRef.setInput('multi', true);
|
|
97
|
+
fixture.detectChanges();
|
|
98
|
+
await fixture.whenStable();
|
|
99
|
+
|
|
100
|
+
accordion.openAll();
|
|
101
|
+
fixture.detectChanges();
|
|
102
|
+
await fixture.whenStable();
|
|
103
|
+
|
|
104
|
+
expect(header('one').getAttribute('aria-expanded')).toBe('true');
|
|
105
|
+
expect(header('two').getAttribute('aria-expanded')).toBe('true');
|
|
106
|
+
|
|
107
|
+
accordion.closeAll();
|
|
108
|
+
fixture.detectChanges();
|
|
109
|
+
await fixture.whenStable();
|
|
110
|
+
expect(header('one').getAttribute('aria-expanded')).toBe('false');
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it('aplica as cores como CSS custom properties', () => {
|
|
114
|
+
const acc = host.querySelector('[data-cy="acc"]') as HTMLElement;
|
|
115
|
+
expect(acc.style.getPropertyValue('--cao-accordion-header-bg')).toBe('#111111');
|
|
116
|
+
});
|
|
117
|
+
});
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ChangeDetectionStrategy,
|
|
3
|
+
Component,
|
|
4
|
+
computed,
|
|
5
|
+
ElementRef,
|
|
6
|
+
forwardRef,
|
|
7
|
+
HostBinding,
|
|
8
|
+
HostListener,
|
|
9
|
+
inject,
|
|
10
|
+
input,
|
|
11
|
+
} from '@angular/core';
|
|
12
|
+
|
|
13
|
+
import {
|
|
14
|
+
CAO_ACCORDION,
|
|
15
|
+
CaoAccordionAlign,
|
|
16
|
+
CaoAccordionDisplayMode,
|
|
17
|
+
CaoAccordionTogglePosition,
|
|
18
|
+
ICaoAccordionHost,
|
|
19
|
+
ICaoAccordionItemRef,
|
|
20
|
+
} from './accordion.token';
|
|
21
|
+
|
|
22
|
+
const HEADER_SELECTOR = '.cao-accordion-item__header:not(:disabled)';
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Container de `<cao-accordion-item>`. Controla o modo de expansão (único ou múltiplo),
|
|
26
|
+
* a navegação por teclado e o tema (cores, posições e visibilidade) herdado pelos itens.
|
|
27
|
+
*/
|
|
28
|
+
@Component({
|
|
29
|
+
selector: 'cao-accordion',
|
|
30
|
+
templateUrl: './accordion.component.html',
|
|
31
|
+
styleUrl: './accordion.component.css',
|
|
32
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
33
|
+
providers: [{ provide: CAO_ACCORDION, useExisting: forwardRef(() => CaoAccordionComponent) }],
|
|
34
|
+
})
|
|
35
|
+
export class CaoAccordionComponent implements ICaoAccordionHost {
|
|
36
|
+
private readonly host = inject<ElementRef<HTMLElement>>(ElementRef);
|
|
37
|
+
private readonly items = new Set<ICaoAccordionItemRef>();
|
|
38
|
+
|
|
39
|
+
// ----- Comportamento -----
|
|
40
|
+
/** Permite vários itens expandidos ao mesmo tempo. */
|
|
41
|
+
readonly multi = input<boolean>(false);
|
|
42
|
+
/** Oculta o ícone de expandir/recolher em todos os itens (cada item pode sobrescrever). */
|
|
43
|
+
readonly hideToggle = input<boolean>(false);
|
|
44
|
+
/** Exibe ou oculta o accordion inteiro. */
|
|
45
|
+
readonly isVisible = input<boolean>(true);
|
|
46
|
+
/** Atributo `data-cy` para testes E2E. */
|
|
47
|
+
readonly dataCy = input<string>('');
|
|
48
|
+
|
|
49
|
+
// ----- Posição / layout -----
|
|
50
|
+
readonly togglePosition = input<CaoAccordionTogglePosition>('after');
|
|
51
|
+
readonly headerAlign = input<CaoAccordionAlign>('start');
|
|
52
|
+
readonly contentAlign = input<CaoAccordionAlign>('start');
|
|
53
|
+
readonly displayMode = input<CaoAccordionDisplayMode>('default');
|
|
54
|
+
|
|
55
|
+
// ----- Cores -----
|
|
56
|
+
readonly headerBgColor = input<string>('');
|
|
57
|
+
readonly headerTextColor = input<string>('');
|
|
58
|
+
readonly headerHoverBgColor = input<string>('');
|
|
59
|
+
readonly headerActiveBgColor = input<string>('');
|
|
60
|
+
readonly activeColor = input<string>('');
|
|
61
|
+
readonly contentBgColor = input<string>('');
|
|
62
|
+
readonly contentTextColor = input<string>('');
|
|
63
|
+
readonly borderColor = input<string>('');
|
|
64
|
+
readonly disabledColor = input<string>('');
|
|
65
|
+
readonly focusRingColor = input<string>('');
|
|
66
|
+
readonly iconColor = input<string>('');
|
|
67
|
+
|
|
68
|
+
// ----- Dimensões / animação -----
|
|
69
|
+
readonly borderRadius = input<string>('');
|
|
70
|
+
readonly gap = input<string>('');
|
|
71
|
+
readonly headerPadding = input<string>('');
|
|
72
|
+
readonly contentPadding = input<string>('');
|
|
73
|
+
readonly animationDuration = input<string>('');
|
|
74
|
+
|
|
75
|
+
private readonly hostStyle = computed(() => ({
|
|
76
|
+
'--cao-accordion-header-bg': this.headerBgColor() || null,
|
|
77
|
+
'--cao-accordion-header-text': this.headerTextColor() || null,
|
|
78
|
+
'--cao-accordion-header-hover-bg': this.headerHoverBgColor() || null,
|
|
79
|
+
'--cao-accordion-header-active-bg': this.headerActiveBgColor() || null,
|
|
80
|
+
'--cao-accordion-active-color': this.activeColor() || null,
|
|
81
|
+
'--cao-accordion-content-bg': this.contentBgColor() || null,
|
|
82
|
+
'--cao-accordion-content-text': this.contentTextColor() || null,
|
|
83
|
+
'--cao-accordion-border-color': this.borderColor() || null,
|
|
84
|
+
'--cao-accordion-disabled-color': this.disabledColor() || null,
|
|
85
|
+
'--cao-accordion-focus-ring': this.focusRingColor() || null,
|
|
86
|
+
'--cao-accordion-icon-color': this.iconColor() || null,
|
|
87
|
+
'--cao-accordion-radius': this.borderRadius() || null,
|
|
88
|
+
'--cao-accordion-gap': this.gap() || null,
|
|
89
|
+
'--cao-accordion-header-padding': this.headerPadding() || null,
|
|
90
|
+
'--cao-accordion-content-padding': this.contentPadding() || null,
|
|
91
|
+
'--cao-accordion-duration': this.animationDuration() || null,
|
|
92
|
+
'--cao-accordion-content-align': this.contentAlign(),
|
|
93
|
+
}));
|
|
94
|
+
|
|
95
|
+
@HostBinding('style')
|
|
96
|
+
get styleBinding(): Record<string, string | null> {
|
|
97
|
+
return this.hostStyle();
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
@HostBinding('attr.data-cy')
|
|
101
|
+
get dataCyAttr(): string | null {
|
|
102
|
+
return this.dataCy() || null;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
@HostBinding('class.cao-accordion--hidden')
|
|
106
|
+
get hiddenClass(): boolean {
|
|
107
|
+
return !this.isVisible();
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
@HostBinding('class.cao-accordion--default')
|
|
111
|
+
get defaultModeClass(): boolean {
|
|
112
|
+
return this.displayMode() === 'default';
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
@HostBinding('class.cao-accordion--flat')
|
|
116
|
+
get flatModeClass(): boolean {
|
|
117
|
+
return this.displayMode() === 'flat';
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
@HostBinding('class.cao-accordion--separated')
|
|
121
|
+
get separatedModeClass(): boolean {
|
|
122
|
+
return this.displayMode() === 'separated';
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/** Expande todos os itens. Só tem efeito quando `multi` está ativo. */
|
|
126
|
+
openAll(): void {
|
|
127
|
+
if (!this.multi()) {
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
this.items.forEach((item) => item.open());
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** Recolhe todos os itens. */
|
|
134
|
+
closeAll(): void {
|
|
135
|
+
this.items.forEach((item) => item.close());
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
register(item: ICaoAccordionItemRef): void {
|
|
139
|
+
this.items.add(item);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
unregister(item: ICaoAccordionItemRef): void {
|
|
143
|
+
this.items.delete(item);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/** Chamado pelos itens ao expandir: no modo único, recolhe os demais. */
|
|
147
|
+
notifyOpened(item: ICaoAccordionItemRef): void {
|
|
148
|
+
if (this.multi()) {
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
this.items.forEach((other) => {
|
|
152
|
+
if (other !== item && other.expanded()) {
|
|
153
|
+
other.close();
|
|
154
|
+
}
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/** Navegação por teclado entre cabeçalhos: ↑ ↓ Home End. */
|
|
159
|
+
@HostListener('keydown', ['$event'])
|
|
160
|
+
protected onKeydown(event: KeyboardEvent): void {
|
|
161
|
+
const root = this.host.nativeElement;
|
|
162
|
+
const target = event.target as HTMLElement | null;
|
|
163
|
+
|
|
164
|
+
if (!target?.classList.contains('cao-accordion-item__header') || target.closest('cao-accordion') !== root) {
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const headers = Array.from(root.querySelectorAll<HTMLElement>(HEADER_SELECTOR)).filter(
|
|
169
|
+
(header) => header.closest('cao-accordion') === root && !header.closest('.cao-accordion-item--hidden'),
|
|
170
|
+
);
|
|
171
|
+
const current = headers.indexOf(target);
|
|
172
|
+
const last = headers.length - 1;
|
|
173
|
+
let next: number;
|
|
174
|
+
|
|
175
|
+
switch (event.key) {
|
|
176
|
+
case 'ArrowDown':
|
|
177
|
+
next = current >= last ? 0 : current + 1;
|
|
178
|
+
break;
|
|
179
|
+
case 'ArrowUp':
|
|
180
|
+
next = current <= 0 ? last : current - 1;
|
|
181
|
+
break;
|
|
182
|
+
case 'Home':
|
|
183
|
+
next = 0;
|
|
184
|
+
break;
|
|
185
|
+
case 'End':
|
|
186
|
+
next = last;
|
|
187
|
+
break;
|
|
188
|
+
default:
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
event.preventDefault();
|
|
193
|
+
headers[next]?.focus();
|
|
194
|
+
}
|
|
195
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { InjectionToken, Signal } from '@angular/core';
|
|
2
|
+
|
|
3
|
+
/** Posição do ícone de expandir/recolher em relação ao texto do cabeçalho. */
|
|
4
|
+
export type CaoAccordionTogglePosition = 'before' | 'after';
|
|
5
|
+
|
|
6
|
+
/** Alinhamento horizontal de texto (cabeçalho ou conteúdo). */
|
|
7
|
+
export type CaoAccordionAlign = 'start' | 'center' | 'end';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Modo de exibição do accordion.
|
|
11
|
+
* - `default`: painéis unidos dentro de um único contorno arredondado.
|
|
12
|
+
* - `flat`: painéis unidos, sem bordas laterais nem cantos arredondados.
|
|
13
|
+
* - `separated`: cada painel é um cartão independente, separado por `gap`.
|
|
14
|
+
*/
|
|
15
|
+
export type CaoAccordionDisplayMode = 'default' | 'flat' | 'separated';
|
|
16
|
+
|
|
17
|
+
/** Contrato mínimo que um item precisa expor para ser controlado pelo accordion. */
|
|
18
|
+
export interface ICaoAccordionItemRef {
|
|
19
|
+
readonly expanded: Signal<boolean>;
|
|
20
|
+
readonly isDisabled: Signal<boolean>;
|
|
21
|
+
open(): void;
|
|
22
|
+
close(): void;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Contrato do accordion consumido pelos itens.
|
|
27
|
+
* Existe para evitar import circular entre `accordion` e `accordion-item`.
|
|
28
|
+
*/
|
|
29
|
+
export interface ICaoAccordionHost {
|
|
30
|
+
readonly multi: Signal<boolean>;
|
|
31
|
+
readonly hideToggle: Signal<boolean>;
|
|
32
|
+
readonly togglePosition: Signal<CaoAccordionTogglePosition>;
|
|
33
|
+
readonly headerAlign: Signal<CaoAccordionAlign>;
|
|
34
|
+
readonly displayMode: Signal<CaoAccordionDisplayMode>;
|
|
35
|
+
register(item: ICaoAccordionItemRef): void;
|
|
36
|
+
unregister(item: ICaoAccordionItemRef): void;
|
|
37
|
+
notifyOpened(item: ICaoAccordionItemRef): void;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export const CAO_ACCORDION = new InjectionToken<ICaoAccordionHost>('CAO_ACCORDION');
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
:host {
|
|
2
|
+
display: block;
|
|
3
|
+
box-sizing: border-box;
|
|
4
|
+
min-width: 0;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
:host(.cao-accordion-item--hidden) {
|
|
8
|
+
display: none;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
.cao-accordion-item {
|
|
12
|
+
background: var(--cao-accordion-content-bg, #ffffff);
|
|
13
|
+
color: var(--cao-accordion-content-text, #374151);
|
|
14
|
+
overflow: hidden;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/* Cartão independente (modo `separated` ou item usado fora de um accordion). */
|
|
18
|
+
:host(.cao-accordion-item--card) .cao-accordion-item {
|
|
19
|
+
border: 1px solid var(--cao-accordion-border-color, #e5e7eb);
|
|
20
|
+
border-radius: var(--cao-accordion-radius, 8px);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
.cao-accordion-item__heading {
|
|
24
|
+
margin: 0;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
.cao-accordion-item__header {
|
|
28
|
+
display: flex;
|
|
29
|
+
align-items: center;
|
|
30
|
+
gap: 12px;
|
|
31
|
+
box-sizing: border-box;
|
|
32
|
+
width: 100%;
|
|
33
|
+
min-height: 48px;
|
|
34
|
+
margin: 0;
|
|
35
|
+
padding: var(--cao-accordion-header-padding, 12px 16px);
|
|
36
|
+
border: 0;
|
|
37
|
+
border-radius: 0;
|
|
38
|
+
background: var(--cao-accordion-header-bg, #ffffff);
|
|
39
|
+
color: var(--cao-accordion-header-text, #111827);
|
|
40
|
+
font: inherit;
|
|
41
|
+
text-align: start;
|
|
42
|
+
cursor: pointer;
|
|
43
|
+
transition:
|
|
44
|
+
background-color var(--cao-accordion-duration, 250ms) ease,
|
|
45
|
+
color var(--cao-accordion-duration, 250ms) ease;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
.cao-accordion-item__header:hover:not(:disabled) {
|
|
49
|
+
background: var(--cao-accordion-header-hover-bg, #f3f4f6);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
.cao-accordion-item__header:focus-visible {
|
|
53
|
+
outline: 2px solid var(--cao-accordion-focus-ring, rgba(79, 70, 229, 0.35));
|
|
54
|
+
outline-offset: -2px;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
.cao-accordion-item__header:disabled {
|
|
58
|
+
color: var(--cao-accordion-disabled-color, #9ca3af);
|
|
59
|
+
cursor: not-allowed;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
:host(.cao-accordion-item--expanded) .cao-accordion-item__header {
|
|
63
|
+
background: var(--cao-accordion-header-active-bg, var(--cao-accordion-header-bg, #ffffff));
|
|
64
|
+
color: var(--cao-accordion-active-color, #4f46e5);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
:host(.cao-accordion-item--expanded) .cao-accordion-item__header:disabled {
|
|
68
|
+
color: var(--cao-accordion-disabled-color, #9ca3af);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
.cao-accordion-item__text {
|
|
72
|
+
display: flex;
|
|
73
|
+
flex: 1 1 auto;
|
|
74
|
+
flex-direction: column;
|
|
75
|
+
gap: 2px;
|
|
76
|
+
min-width: 0;
|
|
77
|
+
text-align: var(--cao-accordion-header-align, start);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
.cao-accordion-item__label {
|
|
81
|
+
font-weight: 600;
|
|
82
|
+
overflow-wrap: anywhere;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
.cao-accordion-item__description {
|
|
86
|
+
font-size: 0.8125rem;
|
|
87
|
+
font-weight: 400;
|
|
88
|
+
opacity: 0.75;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
.cao-accordion-item__icon {
|
|
92
|
+
flex: 0 0 auto;
|
|
93
|
+
color: var(--cao-accordion-icon-color, currentColor);
|
|
94
|
+
transition: transform var(--cao-accordion-duration, 250ms) ease;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
.cao-accordion-item__header--toggle-before .cao-accordion-item__icon {
|
|
98
|
+
order: -1;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
:host(.cao-accordion-item--expanded) .cao-accordion-item__icon {
|
|
102
|
+
transform: rotate(180deg);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/* Painel: anima a altura via grid (0fr -> 1fr), sem medir o conteúdo em JS. */
|
|
106
|
+
.cao-accordion-item__panel {
|
|
107
|
+
display: grid;
|
|
108
|
+
grid-template-rows: 0fr;
|
|
109
|
+
visibility: hidden;
|
|
110
|
+
transition:
|
|
111
|
+
grid-template-rows var(--cao-accordion-duration, 250ms) ease,
|
|
112
|
+
visibility var(--cao-accordion-duration, 250ms);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
:host(.cao-accordion-item--expanded) .cao-accordion-item__panel {
|
|
116
|
+
grid-template-rows: 1fr;
|
|
117
|
+
visibility: visible;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
.cao-accordion-item__panel-inner {
|
|
121
|
+
min-height: 0;
|
|
122
|
+
overflow: hidden;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
.cao-accordion-item__content {
|
|
126
|
+
padding: var(--cao-accordion-content-padding, 16px);
|
|
127
|
+
border-top: 1px solid var(--cao-accordion-border-color, #e5e7eb);
|
|
128
|
+
text-align: var(--cao-accordion-content-align, start);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
@media (prefers-reduced-motion: reduce) {
|
|
132
|
+
.cao-accordion-item__header,
|
|
133
|
+
.cao-accordion-item__icon,
|
|
134
|
+
.cao-accordion-item__panel {
|
|
135
|
+
transition: none;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
<div class="cao-accordion-item">
|
|
2
|
+
<div class="cao-accordion-item__heading" role="heading" [attr.aria-level]="headingLevel()">
|
|
3
|
+
<button
|
|
4
|
+
type="button"
|
|
5
|
+
class="cao-accordion-item__header"
|
|
6
|
+
[class.cao-accordion-item__header--toggle-before]="togglePos() === 'before'"
|
|
7
|
+
[id]="headerId"
|
|
8
|
+
[disabled]="isDisabled()"
|
|
9
|
+
[attr.aria-expanded]="expanded()"
|
|
10
|
+
[attr.aria-controls]="panelId"
|
|
11
|
+
[attr.data-cy]="headerDataCy()"
|
|
12
|
+
(click)="toggle()">
|
|
13
|
+
<span class="cao-accordion-item__text">
|
|
14
|
+
<span class="cao-accordion-item__label">{{ label() }}</span>
|
|
15
|
+
@if (description()) {
|
|
16
|
+
<span class="cao-accordion-item__description">{{ description() }}</span>
|
|
17
|
+
}
|
|
18
|
+
<ng-content select="[caoAccordionHeader]" />
|
|
19
|
+
</span>
|
|
20
|
+
|
|
21
|
+
@if (!toggleHidden()) {
|
|
22
|
+
<svg
|
|
23
|
+
class="cao-accordion-item__icon"
|
|
24
|
+
viewBox="0 0 24 24"
|
|
25
|
+
width="20"
|
|
26
|
+
height="20"
|
|
27
|
+
fill="none"
|
|
28
|
+
stroke="currentColor"
|
|
29
|
+
stroke-width="2"
|
|
30
|
+
stroke-linecap="round"
|
|
31
|
+
stroke-linejoin="round"
|
|
32
|
+
aria-hidden="true"
|
|
33
|
+
focusable="false">
|
|
34
|
+
<path d="m6 9 6 6 6-6" />
|
|
35
|
+
</svg>
|
|
36
|
+
}
|
|
37
|
+
</button>
|
|
38
|
+
</div>
|
|
39
|
+
|
|
40
|
+
<div
|
|
41
|
+
class="cao-accordion-item__panel"
|
|
42
|
+
role="region"
|
|
43
|
+
[id]="panelId"
|
|
44
|
+
[attr.aria-labelledby]="headerId"
|
|
45
|
+
[attr.data-cy]="panelDataCy()">
|
|
46
|
+
<div class="cao-accordion-item__panel-inner">
|
|
47
|
+
<div class="cao-accordion-item__content">
|
|
48
|
+
<ng-content />
|
|
49
|
+
</div>
|
|
50
|
+
</div>
|
|
51
|
+
</div>
|
|
52
|
+
</div>
|