ngx-opalbytes-components 1.28.0 → 1.30.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 +298 -0
- package/cli/cao-comp.cjs +10 -10
- package/fesm2022/ngx-opalbytes-components.mjs +394 -6
- package/fesm2022/ngx-opalbytes-components.mjs.map +1 -1
- package/package.json +1 -1
- package/src/lib/shared/components/avatar/avatar.constants.ts +17 -0
- package/src/lib/shared/components/avatar/avatar.css +44 -0
- package/src/lib/shared/components/avatar/avatar.html +27 -0
- package/src/lib/shared/components/avatar/avatar.model.ts +3 -0
- package/src/lib/shared/components/avatar/avatar.spec.ts +88 -0
- package/src/lib/shared/components/avatar/avatar.stories.ts +85 -0
- package/src/lib/shared/components/avatar/avatar.ts +83 -0
- package/src/lib/shared/components/avatar/index.ts +3 -0
- package/src/lib/shared/components/breadcrumb/breadcrumb.css +105 -0
- package/src/lib/shared/components/breadcrumb/breadcrumb.html +108 -0
- package/src/lib/shared/components/breadcrumb/breadcrumb.model.ts +54 -0
- package/src/lib/shared/components/breadcrumb/breadcrumb.spec.ts +93 -0
- package/src/lib/shared/components/breadcrumb/breadcrumb.stories.ts +122 -0
- package/src/lib/shared/components/breadcrumb/breadcrumb.ts +72 -0
- package/src/lib/shared/components/breadcrumb/index.ts +2 -0
- package/src/lib/shared/components/index.ts +5 -1
- package/src/lib/shared/components/popover/index.ts +3 -0
- package/src/lib/shared/components/popover/popover.css +149 -0
- package/src/lib/shared/components/popover/popover.html +44 -0
- package/src/lib/shared/components/popover/popover.model.ts +19 -0
- package/src/lib/shared/components/popover/popover.spec.ts +22 -0
- package/src/lib/shared/components/popover/popover.stories.ts +0 -0
- package/src/lib/shared/components/popover/popover.ts +312 -0
- package/src/lib/shared/components/progress-bar/index.ts +2 -0
- package/src/lib/shared/components/progress-bar/progress-bar.css +141 -0
- package/src/lib/shared/components/progress-bar/progress-bar.html +39 -0
- package/src/lib/shared/components/progress-bar/progress-bar.model.ts +8 -0
- package/src/lib/shared/components/progress-bar/progress-bar.spec.ts +94 -0
- package/src/lib/shared/components/progress-bar/progress-bar.stories.ts +53 -0
- package/src/lib/shared/components/progress-bar/progress-bar.ts +50 -0
- package/src/lib/shared/components/progress-spinner/index.ts +2 -0
- package/src/lib/shared/components/progress-spinner/progress-spinner.css +90 -0
- package/src/lib/shared/components/progress-spinner/progress-spinner.html +50 -0
- package/src/lib/shared/components/progress-spinner/progress-spinner.model.ts +8 -0
- package/src/lib/shared/components/progress-spinner/progress-spinner.spec.ts +85 -0
- package/src/lib/shared/components/progress-spinner/progress-spinner.stories.ts +73 -0
- package/src/lib/shared/components/progress-spinner/progress-spinner.ts +71 -0
- package/types/ngx-opalbytes-components.d.ts +262 -3
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
|
2
|
+
import { provideRouter } from '@angular/router';
|
|
3
|
+
|
|
4
|
+
import { CaoBreadcrumbComponent } from './breadcrumb';
|
|
5
|
+
import { ICaoBreadcrumbItem } from './breadcrumb.model';
|
|
6
|
+
|
|
7
|
+
describe('CaoBreadcrumbComponent', () => {
|
|
8
|
+
let fixture: ComponentFixture<CaoBreadcrumbComponent>;
|
|
9
|
+
let el: HTMLElement;
|
|
10
|
+
|
|
11
|
+
const items: ICaoBreadcrumbItem[] = [
|
|
12
|
+
{ label: 'Início', link: '/' },
|
|
13
|
+
{ label: 'Clientes', href: 'https://example.com' },
|
|
14
|
+
{ label: 'Detalhe', badge: 3 },
|
|
15
|
+
];
|
|
16
|
+
|
|
17
|
+
beforeEach(async () => {
|
|
18
|
+
await TestBed.configureTestingModule({
|
|
19
|
+
imports: [CaoBreadcrumbComponent],
|
|
20
|
+
providers: [provideRouter([])],
|
|
21
|
+
}).compileComponents();
|
|
22
|
+
|
|
23
|
+
fixture = TestBed.createComponent(CaoBreadcrumbComponent);
|
|
24
|
+
el = fixture.nativeElement;
|
|
25
|
+
fixture.componentRef.setInput('items', items);
|
|
26
|
+
fixture.detectChanges();
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it('renderiza um item por entrada', () => {
|
|
30
|
+
expect(el.querySelectorAll('.cao-breadcrumb__item').length).toBe(3);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it('usa "/" como divisor padrão (n - 1 divisores)', () => {
|
|
34
|
+
const seps = el.querySelectorAll('.cao-breadcrumb__separator');
|
|
35
|
+
expect(seps.length).toBe(2);
|
|
36
|
+
expect(seps[0].textContent?.trim()).toBe('/');
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it('aceita divisor de texto customizado', () => {
|
|
40
|
+
fixture.componentRef.setInput('separator', '>');
|
|
41
|
+
fixture.detectChanges();
|
|
42
|
+
expect(el.querySelector('.cao-breadcrumb__separator')?.textContent?.trim()).toBe('>');
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('marca o último item como página atual e sem link', () => {
|
|
46
|
+
const current = el.querySelector('.cao-breadcrumb__item--current .cao-breadcrumb__text');
|
|
47
|
+
expect(current?.getAttribute('aria-current')).toBe('page');
|
|
48
|
+
expect(el.querySelector('.cao-breadcrumb__item--current a')).toBeNull();
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it('renderiza badge', () => {
|
|
52
|
+
expect(el.querySelector('.cao-breadcrumb__badge')?.textContent?.trim()).toBe('3');
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('inclui o item home como primeiro', () => {
|
|
56
|
+
fixture.componentRef.setInput('home', { label: 'Home', link: '/' });
|
|
57
|
+
fixture.detectChanges();
|
|
58
|
+
const first = el.querySelector('.cao-breadcrumb__item');
|
|
59
|
+
expect(first?.textContent).toContain('Home');
|
|
60
|
+
expect(el.querySelectorAll('.cao-breadcrumb__item').length).toBe(4);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('emite itemClick e executa command', () => {
|
|
64
|
+
let commandCalled = false;
|
|
65
|
+
const emitted: unknown[] = [];
|
|
66
|
+
fixture.componentRef.setInput('items', [
|
|
67
|
+
{ label: 'Ação', command: () => (commandCalled = true) },
|
|
68
|
+
{ label: 'Atual' },
|
|
69
|
+
]);
|
|
70
|
+
fixture.componentInstance.itemClick.subscribe((e) => emitted.push(e));
|
|
71
|
+
fixture.detectChanges();
|
|
72
|
+
|
|
73
|
+
el.querySelector<HTMLButtonElement>('button.cao-breadcrumb__action')?.click();
|
|
74
|
+
|
|
75
|
+
expect(commandCalled).toBe(true);
|
|
76
|
+
expect(emitted.length).toBe(1);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it('não torna interativo um item desabilitado', () => {
|
|
80
|
+
fixture.componentRef.setInput('items', [
|
|
81
|
+
{ label: 'Bloqueado', link: '/x', disabled: true },
|
|
82
|
+
{ label: 'Atual' },
|
|
83
|
+
]);
|
|
84
|
+
fixture.detectChanges();
|
|
85
|
+
expect(el.querySelector('a, button')).toBeNull();
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it('não renderiza nada sem itens', () => {
|
|
89
|
+
fixture.componentRef.setInput('items', []);
|
|
90
|
+
fixture.detectChanges();
|
|
91
|
+
expect(el.querySelector('nav')).toBeNull();
|
|
92
|
+
});
|
|
93
|
+
});
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { provideRouter } from '@angular/router';
|
|
2
|
+
|
|
3
|
+
import { provideLucideIcons, LucideBuilding2, LucideChevronRight, LucideFolder, LucideHouse, LucideUser } from '@lucide/angular';
|
|
4
|
+
import { applicationConfig, argsToTemplate, Meta, StoryObj } from '@storybook/angular-vite';
|
|
5
|
+
|
|
6
|
+
import { CaoBreadcrumbComponent } from './breadcrumb';
|
|
7
|
+
import { ICaoBreadcrumbItem } from './breadcrumb.model';
|
|
8
|
+
|
|
9
|
+
/* eslint-disable @typescript-eslint/naming-convention */
|
|
10
|
+
const STORY_ICONS = { LucideBuilding2, LucideChevronRight, LucideFolder, LucideHouse, LucideUser };
|
|
11
|
+
/* eslint-enable @typescript-eslint/naming-convention */
|
|
12
|
+
|
|
13
|
+
const IMG_ICON =
|
|
14
|
+
`data:image/svg+xml;utf8,${encodeURIComponent(
|
|
15
|
+
'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><circle cx="12" cy="12" r="10" fill="#4f46e5"/><path d="M8 12l3 3 5-6" stroke="#fff" stroke-width="2" fill="none"/></svg>',
|
|
16
|
+
)}`;
|
|
17
|
+
|
|
18
|
+
const basicItems: ICaoBreadcrumbItem[] = [
|
|
19
|
+
{ label: 'Início', link: '/' },
|
|
20
|
+
{ label: 'Clientes', link: '/clientes' },
|
|
21
|
+
{ label: 'Maria Souza' },
|
|
22
|
+
];
|
|
23
|
+
|
|
24
|
+
const meta: Meta<CaoBreadcrumbComponent> = {
|
|
25
|
+
title: 'Componentes/Breadcrumb',
|
|
26
|
+
component: CaoBreadcrumbComponent,
|
|
27
|
+
tags: ['autodocs'],
|
|
28
|
+
decorators: [
|
|
29
|
+
applicationConfig({
|
|
30
|
+
providers: [
|
|
31
|
+
provideRouter([]),
|
|
32
|
+
provideLucideIcons(STORY_ICONS as never),
|
|
33
|
+
],
|
|
34
|
+
}),
|
|
35
|
+
],
|
|
36
|
+
render: (args) => ({ props: args, template: `<cao-breadcrumb ${argsToTemplate(args)} />` }),
|
|
37
|
+
argTypes: {
|
|
38
|
+
items: { control: 'object', description: 'Lista de itens (o último é a página atual).' },
|
|
39
|
+
home: { control: 'object', description: 'Item home opcional, exibido antes dos itens.' },
|
|
40
|
+
separator: { control: 'text', description: 'Texto do divisor. Padrão: "/".' },
|
|
41
|
+
separatorIcon: { control: 'text', description: 'Ícone do divisor (Lucide ou imagem). Sobrepõe `separator`.' },
|
|
42
|
+
isLucideSeparatorIcon: { control: 'boolean' },
|
|
43
|
+
separatorIconSize: { control: 'number' },
|
|
44
|
+
linkColor: { control: 'color' },
|
|
45
|
+
linkHoverColor: { control: 'color' },
|
|
46
|
+
currentColor: { control: 'color' },
|
|
47
|
+
separatorColor: { control: 'color' },
|
|
48
|
+
fontSize: { control: 'text' },
|
|
49
|
+
gap: { control: 'text' },
|
|
50
|
+
ariaLabel: { control: 'text' },
|
|
51
|
+
itemClick: { action: 'itemClick' },
|
|
52
|
+
},
|
|
53
|
+
args: { items: basicItems, separator: '/', ariaLabel: 'Breadcrumb' },
|
|
54
|
+
};
|
|
55
|
+
export default meta;
|
|
56
|
+
|
|
57
|
+
type Story = StoryObj<CaoBreadcrumbComponent>;
|
|
58
|
+
|
|
59
|
+
export const Default: Story = {};
|
|
60
|
+
|
|
61
|
+
export const WithHome: Story = {
|
|
62
|
+
args: { home: { icon: 'house', ariaLabel: 'Início', link: '/' }, items: basicItems.slice(1) },
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
export const IconsLeftAndRight: Story = {
|
|
66
|
+
args: {
|
|
67
|
+
items: [
|
|
68
|
+
{ label: 'Empresa', icon: 'building-2', iconPosition: 'left', link: '/empresa' },
|
|
69
|
+
{ label: 'Projetos', icon: 'folder', iconPosition: 'right', link: '/projetos' },
|
|
70
|
+
{ label: 'Perfil', icon: 'user', iconPosition: 'left' },
|
|
71
|
+
],
|
|
72
|
+
},
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
export const ImageIcon: Story = {
|
|
76
|
+
args: {
|
|
77
|
+
items: [
|
|
78
|
+
{ label: 'Início', icon: IMG_ICON, isLucideIcon: false, link: '/' },
|
|
79
|
+
{ label: 'Relatórios', icon: IMG_ICON, isLucideIcon: false, iconPosition: 'right', link: '/relatorios' },
|
|
80
|
+
{ label: 'Mensal' },
|
|
81
|
+
],
|
|
82
|
+
},
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
export const WithBadges: Story = {
|
|
86
|
+
args: {
|
|
87
|
+
items: [
|
|
88
|
+
{ label: 'Início', link: '/' },
|
|
89
|
+
{ label: 'Pedidos', link: '/pedidos', badge: 12 },
|
|
90
|
+
{ label: 'Pendentes', badge: 'novo', badgeBgColor: '#dcfce7', badgeColor: '#166534' },
|
|
91
|
+
],
|
|
92
|
+
},
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
export const CustomTextSeparator: Story = { args: { separator: '>' } };
|
|
96
|
+
|
|
97
|
+
export const IconSeparator: Story = {
|
|
98
|
+
args: { separatorIcon: 'chevron-right', isLucideSeparatorIcon: true, separatorColor: '#6b7280' },
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
export const ExternalLinkAndCommand: Story = {
|
|
102
|
+
args: {
|
|
103
|
+
items: [
|
|
104
|
+
{ label: 'Docs', href: 'https://angular.dev', target: '_blank' },
|
|
105
|
+
{ label: 'Ação customizada', command: () => console.warn('command executado') },
|
|
106
|
+
{ label: 'Desabilitado', link: '/x', disabled: true },
|
|
107
|
+
{ label: 'Atual' },
|
|
108
|
+
],
|
|
109
|
+
},
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
export const Themed: Story = {
|
|
113
|
+
args: {
|
|
114
|
+
linkColor: '#0f766e',
|
|
115
|
+
linkHoverColor: '#134e4a',
|
|
116
|
+
currentColor: '#042f2e',
|
|
117
|
+
separatorColor: '#5eead4',
|
|
118
|
+
fontSize: '1rem',
|
|
119
|
+
gap: '12px',
|
|
120
|
+
separatorIcon: 'chevron-right',
|
|
121
|
+
},
|
|
122
|
+
};
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { NgTemplateOutlet } from '@angular/common';
|
|
2
|
+
import { ChangeDetectionStrategy, Component, computed, input, output } from '@angular/core';
|
|
3
|
+
import { RouterLink } from '@angular/router';
|
|
4
|
+
|
|
5
|
+
import { ICaoBreadcrumbItem, ICaoBreadcrumbItemClickEvent } from './breadcrumb.model';
|
|
6
|
+
import { CaoBaseIcon } from '../base-icon/base-icon';
|
|
7
|
+
|
|
8
|
+
@Component({
|
|
9
|
+
selector: 'cao-breadcrumb',
|
|
10
|
+
imports: [NgTemplateOutlet, RouterLink, CaoBaseIcon],
|
|
11
|
+
templateUrl: './breadcrumb.html',
|
|
12
|
+
styleUrl: './breadcrumb.css',
|
|
13
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
14
|
+
})
|
|
15
|
+
export class CaoBreadcrumbComponent {
|
|
16
|
+
/** Itens do breadcrumb. O último item é tratado como página atual. */
|
|
17
|
+
readonly items = input<ICaoBreadcrumbItem[]>([]);
|
|
18
|
+
/** Item "home" opcional, sempre exibido como primeiro item. */
|
|
19
|
+
readonly home = input<ICaoBreadcrumbItem | undefined>(undefined);
|
|
20
|
+
|
|
21
|
+
/** Texto do divisor. Padrão: `'/'`. Ignorado se `separatorIcon` for informado. */
|
|
22
|
+
readonly separator = input<string>('/');
|
|
23
|
+
/** Ícone do divisor (nome Lucide ou imagem). Tem prioridade sobre `separator`. */
|
|
24
|
+
readonly separatorIcon = input<string>('');
|
|
25
|
+
readonly isLucideSeparatorIcon = input<boolean>(true);
|
|
26
|
+
readonly separatorIconSize = input<number>(14);
|
|
27
|
+
readonly separatorIconStrokeWidth = input<number>(1.5);
|
|
28
|
+
|
|
29
|
+
readonly linkColor = input<string>('');
|
|
30
|
+
readonly linkHoverColor = input<string>('');
|
|
31
|
+
readonly currentColor = input<string>('');
|
|
32
|
+
readonly separatorColor = input<string>('');
|
|
33
|
+
readonly fontSize = input<string>('');
|
|
34
|
+
readonly gap = input<string>('');
|
|
35
|
+
|
|
36
|
+
readonly ariaLabel = input<string>('Breadcrumb');
|
|
37
|
+
readonly dataCy = input<string>('');
|
|
38
|
+
|
|
39
|
+
/** Emitido ao clicar em qualquer item interativo. */
|
|
40
|
+
readonly itemClick = output<ICaoBreadcrumbItemClickEvent>();
|
|
41
|
+
|
|
42
|
+
protected readonly allItems = computed<ICaoBreadcrumbItem[]>(() => {
|
|
43
|
+
const home = this.home();
|
|
44
|
+
return home ? [home, ...this.items()] : this.items();
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
protected isInteractive(item: ICaoBreadcrumbItem, isLast: boolean): boolean {
|
|
48
|
+
return !isLast && !item.disabled && (this.hasLink(item) || !!item.href || !!item.command);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
protected hasLink(item: ICaoBreadcrumbItem): boolean {
|
|
52
|
+
return item.link !== undefined && item.link !== null;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
protected onItemClick(item: ICaoBreadcrumbItem, index: number, event: Event): void {
|
|
56
|
+
if (item.disabled) {
|
|
57
|
+
event.preventDefault();
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
const payload: ICaoBreadcrumbItemClickEvent = { item, index, originalEvent: event };
|
|
61
|
+
item.command?.(payload);
|
|
62
|
+
this.itemClick.emit(payload);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
protected iconPosition(item: ICaoBreadcrumbItem): 'left' | 'right' {
|
|
66
|
+
return item.iconPosition ?? 'left';
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
protected isLucide(item: ICaoBreadcrumbItem): boolean {
|
|
70
|
+
return item.isLucideIcon ?? true;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
@@ -34,4 +34,8 @@ export * from "./slide-toggle/slide-toggle.component"
|
|
|
34
34
|
export * from "./radio/radio-group.component"
|
|
35
35
|
export * from "./radio/radio-button.component"
|
|
36
36
|
export * from "./checkbox/checkbox.component"
|
|
37
|
-
export * from "./footer/footer"
|
|
37
|
+
export * from "./footer/footer"
|
|
38
|
+
export * from "./breadcrumb/breadcrumb"
|
|
39
|
+
export * from "./progress-bar/progress-bar"
|
|
40
|
+
export * from "./progress-spinner/progress-spinner"
|
|
41
|
+
export * from "./popover/popover"
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
:host {
|
|
2
|
+
display: contents;
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
.cao-popover {
|
|
6
|
+
--_bg: var(--cao-popover-bg, #ffffff);
|
|
7
|
+
--_text: var(--cao-popover-text, #111827);
|
|
8
|
+
--_border: var(--cao-popover-border-color, #e5e7eb);
|
|
9
|
+
--_radius: var(--cao-popover-radius, 8px);
|
|
10
|
+
--_padding: var(--cao-popover-padding, 12px 16px);
|
|
11
|
+
--_shadow: var(--cao-popover-shadow, 0 10px 25px -5px rgba(17, 24, 39, 0.15), 0 4px 10px -6px rgba(17, 24, 39, 0.12));
|
|
12
|
+
--_focus: var(--cao-popover-focus-ring, rgba(79, 70, 229, 0.35));
|
|
13
|
+
--_arrow-size: 10px;
|
|
14
|
+
--_arrow-offset: 16px;
|
|
15
|
+
|
|
16
|
+
position: relative;
|
|
17
|
+
box-sizing: border-box;
|
|
18
|
+
border: 1px solid var(--_border);
|
|
19
|
+
border-radius: var(--_radius);
|
|
20
|
+
background-color: var(--_bg);
|
|
21
|
+
box-shadow: var(--_shadow);
|
|
22
|
+
color: var(--_text);
|
|
23
|
+
font-size: 0.875rem;
|
|
24
|
+
line-height: 1.5;
|
|
25
|
+
outline: none;
|
|
26
|
+
animation: cao-popover-in 150ms ease-out;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
.cao-popover:focus-visible {
|
|
30
|
+
box-shadow: var(--_shadow), 0 0 0 3px var(--_focus);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
.cao-popover__content {
|
|
34
|
+
padding: var(--_padding);
|
|
35
|
+
overflow-wrap: anywhere;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
.cao-popover__content--with-close {
|
|
39
|
+
padding-right: 40px;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
.cao-popover__close {
|
|
43
|
+
position: absolute;
|
|
44
|
+
top: 8px;
|
|
45
|
+
right: 8px;
|
|
46
|
+
display: inline-flex;
|
|
47
|
+
align-items: center;
|
|
48
|
+
justify-content: center;
|
|
49
|
+
width: 24px;
|
|
50
|
+
height: 24px;
|
|
51
|
+
padding: 0;
|
|
52
|
+
border: 0;
|
|
53
|
+
border-radius: 4px;
|
|
54
|
+
background: none;
|
|
55
|
+
color: #6b7280;
|
|
56
|
+
cursor: pointer;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
.cao-popover__close:hover {
|
|
60
|
+
background-color: #f3f4f6;
|
|
61
|
+
color: #111827;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
.cao-popover__close:focus-visible {
|
|
65
|
+
outline: 2px solid var(--_focus);
|
|
66
|
+
outline-offset: 1px;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/* Seta */
|
|
70
|
+
.cao-popover__arrow {
|
|
71
|
+
position: absolute;
|
|
72
|
+
width: var(--_arrow-size);
|
|
73
|
+
height: var(--_arrow-size);
|
|
74
|
+
background-color: var(--_bg);
|
|
75
|
+
transform: rotate(45deg);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
.cao-popover--bottom .cao-popover__arrow {
|
|
79
|
+
top: calc(var(--_arrow-size) / -2 - 1px);
|
|
80
|
+
border-top: 1px solid var(--_border);
|
|
81
|
+
border-left: 1px solid var(--_border);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
.cao-popover--top .cao-popover__arrow {
|
|
85
|
+
bottom: calc(var(--_arrow-size) / -2 - 1px);
|
|
86
|
+
border-right: 1px solid var(--_border);
|
|
87
|
+
border-bottom: 1px solid var(--_border);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
.cao-popover--right .cao-popover__arrow {
|
|
91
|
+
left: calc(var(--_arrow-size) / -2 - 1px);
|
|
92
|
+
border-bottom: 1px solid var(--_border);
|
|
93
|
+
border-left: 1px solid var(--_border);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
.cao-popover--left .cao-popover__arrow {
|
|
97
|
+
right: calc(var(--_arrow-size) / -2 - 1px);
|
|
98
|
+
border-top: 1px solid var(--_border);
|
|
99
|
+
border-right: 1px solid var(--_border);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/* Posição da seta ao longo da borda (topo/base) */
|
|
103
|
+
.cao-popover--top.cao-popover--align-center .cao-popover__arrow,
|
|
104
|
+
.cao-popover--bottom.cao-popover--align-center .cao-popover__arrow {
|
|
105
|
+
left: calc(50% - var(--_arrow-size) / 2);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
.cao-popover--top.cao-popover--align-start .cao-popover__arrow,
|
|
109
|
+
.cao-popover--bottom.cao-popover--align-start .cao-popover__arrow {
|
|
110
|
+
left: var(--_arrow-offset);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
.cao-popover--top.cao-popover--align-end .cao-popover__arrow,
|
|
114
|
+
.cao-popover--bottom.cao-popover--align-end .cao-popover__arrow {
|
|
115
|
+
right: var(--_arrow-offset);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/* Posição da seta ao longo da borda (esquerda/direita) */
|
|
119
|
+
.cao-popover--left.cao-popover--align-center .cao-popover__arrow,
|
|
120
|
+
.cao-popover--right.cao-popover--align-center .cao-popover__arrow {
|
|
121
|
+
top: calc(50% - var(--_arrow-size) / 2);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
.cao-popover--left.cao-popover--align-start .cao-popover__arrow,
|
|
125
|
+
.cao-popover--right.cao-popover--align-start .cao-popover__arrow {
|
|
126
|
+
top: var(--_arrow-offset);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
.cao-popover--left.cao-popover--align-end .cao-popover__arrow,
|
|
130
|
+
.cao-popover--right.cao-popover--align-end .cao-popover__arrow {
|
|
131
|
+
bottom: var(--_arrow-offset);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
@keyframes cao-popover-in {
|
|
135
|
+
from {
|
|
136
|
+
opacity: 0;
|
|
137
|
+
transform: scale(0.97);
|
|
138
|
+
}
|
|
139
|
+
to {
|
|
140
|
+
opacity: 1;
|
|
141
|
+
transform: scale(1);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
@media (prefers-reduced-motion: reduce) {
|
|
146
|
+
.cao-popover {
|
|
147
|
+
animation: none;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
<ng-template #panelTemplate>
|
|
2
|
+
<div
|
|
3
|
+
[ngClass]="'cao-popover'"
|
|
4
|
+
tabindex="-1"
|
|
5
|
+
[id]="panelId"
|
|
6
|
+
[class]="styleClass()"
|
|
7
|
+
[class.cao-popover--top]="placement() === 'top'"
|
|
8
|
+
[class.cao-popover--bottom]="placement() === 'bottom'"
|
|
9
|
+
[class.cao-popover--left]="placement() === 'left'"
|
|
10
|
+
[class.cao-popover--right]="placement() === 'right'"
|
|
11
|
+
[class.cao-popover--align-start]="align() === 'start'"
|
|
12
|
+
[class.cao-popover--align-center]="align() === 'center'"
|
|
13
|
+
[class.cao-popover--align-end]="align() === 'end'"
|
|
14
|
+
[class.cao-popover--no-arrow]="!showArrow()"
|
|
15
|
+
[attr.role]="role() || null"
|
|
16
|
+
[attr.aria-label]="ariaLabel() || null"
|
|
17
|
+
[attr.aria-labelledby]="ariaLabelledBy() || null"
|
|
18
|
+
[attr.data-cy]="dataCy() || null"
|
|
19
|
+
[style.width]="width() || null"
|
|
20
|
+
[style.max-width]="maxWidth() || null"
|
|
21
|
+
[style.--cao-popover-bg]="bgColor() || null"
|
|
22
|
+
[style.--cao-popover-text]="textColor() || null"
|
|
23
|
+
[style.--cao-popover-border-color]="borderColor() || null"
|
|
24
|
+
[style.--cao-popover-radius]="borderRadius() || null"
|
|
25
|
+
[style.--cao-popover-padding]="padding() || null"
|
|
26
|
+
(mouseenter)="panelHoverChange.emit(true)"
|
|
27
|
+
(mouseleave)="panelHoverChange.emit(false)">
|
|
28
|
+
@if (showArrow()) {
|
|
29
|
+
<span class="cao-popover__arrow" aria-hidden="true"></span>
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
<div class="cao-popover__content" [class.cao-popover__content--with-close]="showCloseIcon()">
|
|
33
|
+
<ng-content />
|
|
34
|
+
</div>
|
|
35
|
+
|
|
36
|
+
@if (showCloseIcon()) {
|
|
37
|
+
<button type="button" class="cao-popover__close" [attr.aria-label]="closeAriaLabel()" (click)="onCloseClick()">
|
|
38
|
+
<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" aria-hidden="true" focusable="false">
|
|
39
|
+
<path d="M6 6l12 12M18 6L6 18" />
|
|
40
|
+
</svg>
|
|
41
|
+
</button>
|
|
42
|
+
}
|
|
43
|
+
</div>
|
|
44
|
+
</ng-template>
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { ElementRef } from '@angular/core';
|
|
2
|
+
|
|
3
|
+
/** Lado do elemento de origem em que o popover é exibido. */
|
|
4
|
+
export type CaoPopoverPosition = 'top' | 'bottom' | 'left' | 'right';
|
|
5
|
+
|
|
6
|
+
/** Alinhamento do popover em relação ao elemento de origem. */
|
|
7
|
+
export type CaoPopoverAlign = 'start' | 'center' | 'end';
|
|
8
|
+
|
|
9
|
+
/** Evento que aciona o popover quando usado com `CaoPopoverTriggerDirective`. */
|
|
10
|
+
export type CaoPopoverTrigger = 'click' | 'hover' | 'focus';
|
|
11
|
+
|
|
12
|
+
/** Elemento de origem (âncora) do popover. */
|
|
13
|
+
export type CaoPopoverTarget = HTMLElement | ElementRef<HTMLElement> | null | undefined;
|
|
14
|
+
|
|
15
|
+
export interface ICaoPopoverShowOptions {
|
|
16
|
+
/** Move o foco para dentro do popover ao abrir. Padrão: valor de `focusOnShow`. */
|
|
17
|
+
focus?: boolean;
|
|
18
|
+
}
|
|
19
|
+
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
|
2
|
+
|
|
3
|
+
import { CaoPopoverComponent } from './popover';
|
|
4
|
+
|
|
5
|
+
describe('CaoPopover', () => {
|
|
6
|
+
let component: CaoPopoverComponent;
|
|
7
|
+
let fixture: ComponentFixture<CaoPopoverComponent>;
|
|
8
|
+
|
|
9
|
+
beforeEach(async () => {
|
|
10
|
+
await TestBed.configureTestingModule({
|
|
11
|
+
imports: [CaoPopoverComponent],
|
|
12
|
+
}).compileComponents();
|
|
13
|
+
|
|
14
|
+
fixture = TestBed.createComponent(CaoPopoverComponent);
|
|
15
|
+
component = fixture.componentInstance;
|
|
16
|
+
await fixture.whenStable();
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it('should create', () => {
|
|
20
|
+
expect(component).toBeTruthy();
|
|
21
|
+
});
|
|
22
|
+
});
|
|
File without changes
|