@block-auth.io/angular 0.1.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/.turbo/turbo-build.log +37 -0
- package/.turbo/turbo-lint.log +4 -0
- package/.turbo/turbo-test.log +82 -0
- package/.turbo/turbo-typecheck.log +4 -0
- package/CHANGELOG.md +10 -0
- package/ng-package.json +6 -0
- package/package.json +50 -0
- package/scripts/css-to-ts.mjs +14 -0
- package/src/lib/__tests__/integration.test.ts +45 -0
- package/src/lib/block-auth.service.test.ts +69 -0
- package/src/lib/block-auth.service.ts +81 -0
- package/src/lib/components/btns/btn-base-provider.component.test.ts +22 -0
- package/src/lib/components/btns/btn-base-provider.component.ts +55 -0
- package/src/lib/components/btns/btn-block-auth.component.test.ts +79 -0
- package/src/lib/components/btns/btn-block-auth.component.ts +104 -0
- package/src/lib/components/btns/btn-gmail.component.test.ts +16 -0
- package/src/lib/components/btns/btn-gmail.component.ts +41 -0
- package/src/lib/components/btns/btn-google-base.component.test.ts +16 -0
- package/src/lib/components/btns/btn-google-base.component.ts +73 -0
- package/src/lib/components/btns/btn-metamask.component.test.ts +20 -0
- package/src/lib/components/btns/btn-metamask.component.ts +98 -0
- package/src/lib/components/btns/btn-microsoft.component.test.ts +16 -0
- package/src/lib/components/btns/btn-microsoft.component.ts +77 -0
- package/src/lib/components/btns/btn-phantom.component.test.ts +20 -0
- package/src/lib/components/btns/btn-phantom.component.ts +93 -0
- package/src/lib/components/btns/btn-signin.component.test.ts +55 -0
- package/src/lib/components/btns/btn-signin.component.ts +108 -0
- package/src/lib/components/btns/btn-wallet-connect.component.test.ts +20 -0
- package/src/lib/components/btns/btn-wallet-connect.component.ts +109 -0
- package/src/lib/components/btns/index.ts +11 -0
- package/src/lib/components/contents/content-signin.component.test.ts +33 -0
- package/src/lib/components/contents/content-signin.component.ts +68 -0
- package/src/lib/components/contents/content-signup.component.test.ts +78 -0
- package/src/lib/components/contents/content-signup.component.ts +225 -0
- package/src/lib/components/dds/change-account.component.test.ts +66 -0
- package/src/lib/components/dds/change-account.component.ts +112 -0
- package/src/lib/components/dds/dropdown-profile-btn.component.test.ts +66 -0
- package/src/lib/components/dds/dropdown-profile-btn.component.ts +185 -0
- package/src/lib/components/dds/dropdown-profile-item.component.test.ts +75 -0
- package/src/lib/components/dds/dropdown-profile-item.component.ts +74 -0
- package/src/lib/components/dds/dropdown-profile.component.test.ts +98 -0
- package/src/lib/components/dds/dropdown-profile.component.ts +258 -0
- package/src/lib/components/flow-block-auth.component.test.ts +93 -0
- package/src/lib/components/flow-block-auth.component.ts +226 -0
- package/src/lib/components/modals/modal-overlay.component.test.ts +81 -0
- package/src/lib/components/modals/modal-overlay.component.ts +43 -0
- package/src/lib/components/modals/modal-restoration-expired.component.test.ts +71 -0
- package/src/lib/components/modals/modal-restoration-expired.component.ts +83 -0
- package/src/lib/components/modals/modal-restore.component.test.ts +74 -0
- package/src/lib/components/modals/modal-restore.component.ts +104 -0
- package/src/lib/components/modals/modal-signin.component.test.ts +66 -0
- package/src/lib/components/modals/modal-signin.component.ts +44 -0
- package/src/lib/components/modals/modal-signup.component.test.ts +56 -0
- package/src/lib/components/modals/modal-signup.component.ts +40 -0
- package/src/lib/components/sections/footer-section.component.test.ts +19 -0
- package/src/lib/components/sections/footer-section.component.ts +49 -0
- package/src/lib/components/sections/internals/providers-buttons.component.test.ts +27 -0
- package/src/lib/components/sections/internals/providers-buttons.component.ts +290 -0
- package/src/lib/components/sections/internals/providers-filters.component.test.ts +25 -0
- package/src/lib/components/sections/internals/providers-filters.component.ts +139 -0
- package/src/lib/components/sections/internals/selected-button.component.test.ts +36 -0
- package/src/lib/components/sections/internals/selected-button.component.ts +92 -0
- package/src/lib/components/sections/login-section.component.test.ts +49 -0
- package/src/lib/components/sections/login-section.component.ts +120 -0
- package/src/lib/components/sections/providers-section.component.test.ts +22 -0
- package/src/lib/components/sections/providers-section.component.ts +187 -0
- package/src/lib/components/txns/internals/permissions-required-modal.component.test.ts +14 -0
- package/src/lib/components/txns/internals/permissions-required-modal.component.ts +35 -0
- package/src/lib/components/txns/internals/solicited-data-modal.component.test.ts +72 -0
- package/src/lib/components/txns/internals/solicited-data-modal.component.ts +187 -0
- package/src/lib/components/txns/internals/txn-error.component.test.ts +100 -0
- package/src/lib/components/txns/internals/txn-error.component.ts +121 -0
- package/src/lib/components/txns/internals/txn-type.component.test.ts +52 -0
- package/src/lib/components/txns/internals/txn-type.component.ts +51 -0
- package/src/lib/components/txns/internals/txn.component.test.ts +188 -0
- package/src/lib/components/txns/internals/txn.component.ts +298 -0
- package/src/lib/components/txns/txns-overview.component.test.ts +107 -0
- package/src/lib/components/txns/txns-overview.component.ts +229 -0
- package/src/lib/icons/ai-outline-disconnect.component.ts +21 -0
- package/src/lib/icons/bi-logo-gmail.component.ts +21 -0
- package/src/lib/icons/cg-profile.component.ts +31 -0
- package/src/lib/icons/check-icon.component.ts +27 -0
- package/src/lib/icons/ci-clock2.component.ts +31 -0
- package/src/lib/icons/fa-arrow-right.component.ts +21 -0
- package/src/lib/icons/fa-times.component.ts +21 -0
- package/src/lib/icons/fa-undo.component.ts +21 -0
- package/src/lib/icons/icons.test.ts +25 -0
- package/src/lib/icons/index.ts +22 -0
- package/src/lib/icons/io-ios-arrow-back.component.ts +21 -0
- package/src/lib/icons/io-ios-arrow-forward.component.ts +21 -0
- package/src/lib/icons/io-md-arrow-back.component.ts +19 -0
- package/src/lib/icons/io-md-arrow-forward.component.ts +19 -0
- package/src/lib/icons/pi-warning-fill.component.ts +21 -0
- package/src/lib/icons/tb-arrows-exchange2.component.ts +30 -0
- package/src/lib/icons/tb-copy-check-filled.component.ts +21 -0
- package/src/lib/icons/tb-copy.component.ts +34 -0
- package/src/lib/icons/ti-vendor-microsoft.component.ts +21 -0
- package/src/lib/inject-store.test.ts +26 -0
- package/src/lib/inject-store.ts +16 -0
- package/src/lib/primitives/arrow-icon.component.test.ts +12 -0
- package/src/lib/primitives/arrow-icon.component.ts +38 -0
- package/src/lib/primitives/btn-arrow-dir.component.test.ts +22 -0
- package/src/lib/primitives/btn-arrow-dir.component.ts +65 -0
- package/src/lib/primitives/btn-base.component.test.ts +38 -0
- package/src/lib/primitives/btn-base.component.ts +140 -0
- package/src/lib/primitives/divider-base.component.test.ts +12 -0
- package/src/lib/primitives/divider-base.component.ts +74 -0
- package/src/lib/primitives/dropdown-arrow.component.test.ts +12 -0
- package/src/lib/primitives/dropdown-arrow.component.ts +46 -0
- package/src/lib/primitives/dropdown-base.component.test.ts +49 -0
- package/src/lib/primitives/dropdown-base.component.ts +329 -0
- package/src/lib/primitives/helpers.ts +151 -0
- package/src/lib/primitives/horizontal-loader.component.test.ts +19 -0
- package/src/lib/primitives/horizontal-loader.component.ts +70 -0
- package/src/lib/primitives/icon-base.component.test.ts +19 -0
- package/src/lib/primitives/icon-base.component.ts +58 -0
- package/src/lib/primitives/img-loader.component.test.ts +12 -0
- package/src/lib/primitives/img-loader.component.ts +118 -0
- package/src/lib/primitives/index.ts +29 -0
- package/src/lib/primitives/input-base.component.test.ts +20 -0
- package/src/lib/primitives/input-base.component.ts +210 -0
- package/src/lib/primitives/lifetime-slider-base.component.test.ts +88 -0
- package/src/lib/primitives/lifetime-slider-base.component.ts +75 -0
- package/src/lib/primitives/oval-spinner.component.test.ts +9 -0
- package/src/lib/primitives/oval-spinner.component.ts +158 -0
- package/src/lib/primitives/pickers/dropdown-picker-animated-base.component.ts +220 -0
- package/src/lib/primitives/pickers/picker-button.component.ts +74 -0
- package/src/lib/primitives/pickers/time-picker-dropdown-selector.component.ts +188 -0
- package/src/lib/primitives/pickers/time-picker.component.ts +124 -0
- package/src/lib/primitives/pickers/time-unit-selector.component.ts +82 -0
- package/src/lib/primitives/progressbar.component.test.ts +18 -0
- package/src/lib/primitives/progressbar.component.ts +93 -0
- package/src/lib/primitives/providersStyles.ts +268 -0
- package/src/lib/primitives/slider-base.component.ts +56 -0
- package/src/lib/provide-block-auth.test.ts +43 -0
- package/src/lib/provide-block-auth.ts +43 -0
- package/src/lib/runtime/block-auth-runtime.directive.test.ts +89 -0
- package/src/lib/runtime/block-auth-runtime.directive.ts +68 -0
- package/src/lib/teleport-body.directive.test.ts +29 -0
- package/src/lib/teleport-body.directive.ts +21 -0
- package/src/lib/testing.ts +15 -0
- package/src/lib/transitions.css +23 -0
- package/src/public-api.ts +85 -0
- package/src/vitest.setup.ts +8 -0
- package/tailwind.config.cjs +6 -0
- package/tsconfig.json +15 -0
- package/tsconfig.lib.json +9 -0
- package/vite.config.mts +44 -0
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { ChangeDetectionStrategy, Component, input } from '@angular/core';
|
|
2
|
+
|
|
3
|
+
// Icono inline extraído de react-icons/tb (TbCopy), espejo de
|
|
4
|
+
// packages/vue/src/icons/TbCopy.vue (mismo `viewBox`/`d`, trazo en vez de
|
|
5
|
+
// relleno: `fill="none"` + `stroke="currentColor"`).
|
|
6
|
+
// Ver check-icon.component.ts para la explicación del enrutado de `class`.
|
|
7
|
+
@Component({
|
|
8
|
+
selector: 'ba-tb-copy',
|
|
9
|
+
standalone: true,
|
|
10
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
11
|
+
host: { style: 'display: contents' },
|
|
12
|
+
template: `
|
|
13
|
+
<svg
|
|
14
|
+
[class]="class()"
|
|
15
|
+
viewBox="0 0 24 24"
|
|
16
|
+
fill="none"
|
|
17
|
+
stroke="currentColor"
|
|
18
|
+
stroke-width="2"
|
|
19
|
+
stroke-linecap="round"
|
|
20
|
+
stroke-linejoin="round"
|
|
21
|
+
aria-hidden="true"
|
|
22
|
+
>
|
|
23
|
+
<path
|
|
24
|
+
d="M7 7m0 2.667a2.667 2.667 0 0 1 2.667 -2.667h8.666a2.667 2.667 0 0 1 2.667 2.667v8.666a2.667 2.667 0 0 1 -2.667 2.667h-8.666a2.667 2.667 0 0 1 -2.667 -2.667z"
|
|
25
|
+
/>
|
|
26
|
+
<path
|
|
27
|
+
d="M4.012 16.737a2.005 2.005 0 0 1 -1.012 -1.737v-10c0 -1.1 .9 -2 2 -2h10c.75 0 1.158 .385 1.5 1"
|
|
28
|
+
/>
|
|
29
|
+
</svg>
|
|
30
|
+
`,
|
|
31
|
+
})
|
|
32
|
+
export class TbCopyComponent {
|
|
33
|
+
class = input('');
|
|
34
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { ChangeDetectionStrategy, Component, input } from '@angular/core';
|
|
2
|
+
|
|
3
|
+
// Icono inline extraído de react-icons/ti (TiVendorMicrosoft), espejo de
|
|
4
|
+
// packages/vue/src/icons/TiVendorMicrosoft.vue (mismo `viewBox`/`d`).
|
|
5
|
+
// Ver check-icon.component.ts para la explicación del enrutado de `class`.
|
|
6
|
+
@Component({
|
|
7
|
+
selector: 'ba-ti-vendor-microsoft',
|
|
8
|
+
standalone: true,
|
|
9
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
10
|
+
host: { style: 'display: contents' },
|
|
11
|
+
template: `
|
|
12
|
+
<svg [class]="class()" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
|
|
13
|
+
<path
|
|
14
|
+
d="M10 12.5c0-.3-.2-.5-.5-.5h-6c-.3 0-.5.2-.5.5v5c0 .3.2.5.5.6l6 .7c.3 0 .5-.2.5-.4v-5.9zM11.5 12c-.3 0-.5.2-.5.5v5.9c0 .3.2.5.5.6l9 1c.3 0 .5-.2.5-.4v-7c0-.3-.2-.5-.5-.5l-9-.1zM10 4.7c0-.3-.2-.5-.5-.4l-6 .7c-.3 0-.5.2-.5.5v5c0 .3.2.5.5.5h6c.3 0 .5-.2.5-.5v-5.8zM11.5 4.1c-.3 0-.5.3-.5.6v5.9c0 .3.2.5.5.5h9c.3 0 .5-.2.5-.5v-7c0-.3-.2-.5-.5-.4l-9 .9z"
|
|
15
|
+
/>
|
|
16
|
+
</svg>
|
|
17
|
+
`,
|
|
18
|
+
})
|
|
19
|
+
export class TiVendorMicrosoftComponent {
|
|
20
|
+
class = input('');
|
|
21
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { Component } from '@angular/core';
|
|
3
|
+
import { render, screen } from '@testing-library/angular';
|
|
4
|
+
import { atom } from 'nanostores';
|
|
5
|
+
import { injectStore } from './inject-store';
|
|
6
|
+
|
|
7
|
+
const $count = atom(0);
|
|
8
|
+
|
|
9
|
+
@Component({
|
|
10
|
+
selector: 'ba-probe',
|
|
11
|
+
standalone: true,
|
|
12
|
+
template: `<span data-testid="count">{{ count() }}</span>`,
|
|
13
|
+
})
|
|
14
|
+
class ProbeComponent {
|
|
15
|
+
count = injectStore($count);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
describe('injectStore', () => {
|
|
19
|
+
it('refleja el valor inicial y reacciona a cambios del atom', async () => {
|
|
20
|
+
const { detectChanges } = await render(ProbeComponent);
|
|
21
|
+
expect(screen.getByTestId('count').textContent).toBe('0');
|
|
22
|
+
$count.set(5);
|
|
23
|
+
detectChanges();
|
|
24
|
+
expect(screen.getByTestId('count').textContent).toBe('5');
|
|
25
|
+
});
|
|
26
|
+
});
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { DestroyRef, inject, signal, type Signal } from '@angular/core';
|
|
2
|
+
import type { Store, StoreValue } from 'nanostores';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Puente nanostores → Angular Signal (equivalente a useStore de
|
|
6
|
+
* @nanostores/react and /vue). Debe llamarse en contexto de inyección
|
|
7
|
+
* (constructor / inicializador de campo), como cualquier inject().
|
|
8
|
+
* La suscripción se limpia con DestroyRef.
|
|
9
|
+
*/
|
|
10
|
+
export function injectStore<S extends Store>(store: S): Signal<StoreValue<S>> {
|
|
11
|
+
const destroyRef = inject(DestroyRef);
|
|
12
|
+
const state = signal<StoreValue<S>>(store.get());
|
|
13
|
+
const unsubscribe = store.listen((value) => state.set(value));
|
|
14
|
+
destroyRef.onDestroy(unsubscribe);
|
|
15
|
+
return state.asReadonly();
|
|
16
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { Component } from '@angular/core';
|
|
3
|
+
import { render } from '@testing-library/angular';
|
|
4
|
+
import { ArrowIconComponent } from './arrow-icon.component';
|
|
5
|
+
describe('ArrowIconComponent', () => {
|
|
6
|
+
it('renderiza la flecha con la clase segun la direccion', async () => {
|
|
7
|
+
@Component({ standalone: true, imports: [ArrowIconComponent], template: `<ba-arrow-icon direction="left" />` })
|
|
8
|
+
class Host {}
|
|
9
|
+
const { container } = await render(Host);
|
|
10
|
+
expect(container.querySelector('.bal-ba-arrow-left')).toBeTruthy();
|
|
11
|
+
});
|
|
12
|
+
});
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { ChangeDetectionStrategy, Component, computed, input } from '@angular/core';
|
|
2
|
+
|
|
3
|
+
// Port directo de ArrowIcon.tsx / ArrowIcon.vue (triple-chevron animado con
|
|
4
|
+
// las clases `bal-ba-arrow*` absorbidas en lib-styles.css).
|
|
5
|
+
@Component({
|
|
6
|
+
selector: 'ba-arrow-icon',
|
|
7
|
+
standalone: true,
|
|
8
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
9
|
+
template: `
|
|
10
|
+
<div class="bas-flex bas-h-[30px] bas-w-[65px] bas-items-center bas-justify-center">
|
|
11
|
+
<div
|
|
12
|
+
[class]="
|
|
13
|
+
'bal-ba-arrow bas-relative bas-flex bas-h-[65px] bas-w-[65px] bas-cursor-pointer bas-justify-center ' +
|
|
14
|
+
(direction() === 'right' ? 'bal-ba-arrow-right bas-items-start' : '') +
|
|
15
|
+
' ' +
|
|
16
|
+
(direction() === 'left' ? 'bal-ba-arrow-left bas-items-center' : '')
|
|
17
|
+
"
|
|
18
|
+
>
|
|
19
|
+
<span [class]="spanClassNames()"></span>
|
|
20
|
+
<span [class]="spanClassNames()"></span>
|
|
21
|
+
<span [class]="spanClassNames()"></span>
|
|
22
|
+
</div>
|
|
23
|
+
</div>
|
|
24
|
+
`,
|
|
25
|
+
})
|
|
26
|
+
export class ArrowIconComponent {
|
|
27
|
+
direction = input<'right' | 'left'>('right');
|
|
28
|
+
|
|
29
|
+
spanClassNames = computed(
|
|
30
|
+
() =>
|
|
31
|
+
`bas-block bas-w-2.5 bas-h-2.5 bas-border-solid
|
|
32
|
+
bas-border-[var(--bal-primary-500)] bas-absolute -bas-m-2.5 ${
|
|
33
|
+
this.direction() === 'right'
|
|
34
|
+
? 'bas-border-t-[2.5px] bas-border-l-[2.5px] bas-border-b-0 bas-border-r-0'
|
|
35
|
+
: 'bas-border-b-[2.5px] bas-border-r-[2.5px] bas-border-t-0 bas-border-l-0'
|
|
36
|
+
}`,
|
|
37
|
+
);
|
|
38
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from 'vitest';
|
|
2
|
+
import { Component } from '@angular/core';
|
|
3
|
+
import { render, screen, fireEvent } from '@testing-library/angular';
|
|
4
|
+
import { BtnArrowDirComponent } from './btn-arrow-dir.component';
|
|
5
|
+
describe('BtnArrowDirComponent', () => {
|
|
6
|
+
it('emite clicked al pulsar si no esta disabled', async () => {
|
|
7
|
+
const onClicked = vi.fn();
|
|
8
|
+
@Component({ standalone: true, imports: [BtnArrowDirComponent], template: `<ba-btn-arrow-dir direction="forward" (clicked)="f()" />` })
|
|
9
|
+
class Host { f = onClicked; }
|
|
10
|
+
await render(Host);
|
|
11
|
+
await fireEvent.click(screen.getByTestId('btn-arrow-forward'));
|
|
12
|
+
expect(onClicked).toHaveBeenCalled();
|
|
13
|
+
});
|
|
14
|
+
it('no emite clicked cuando disabled', async () => {
|
|
15
|
+
const onClicked = vi.fn();
|
|
16
|
+
@Component({ standalone: true, imports: [BtnArrowDirComponent], template: `<ba-btn-arrow-dir [disabled]="true" (clicked)="f()" />` })
|
|
17
|
+
class Host { f = onClicked; }
|
|
18
|
+
await render(Host);
|
|
19
|
+
await fireEvent.click(screen.getByTestId('btn-arrow-back'));
|
|
20
|
+
expect(onClicked).not.toHaveBeenCalled();
|
|
21
|
+
});
|
|
22
|
+
});
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { ChangeDetectionStrategy, Component, computed, input, output } from '@angular/core';
|
|
2
|
+
import {
|
|
3
|
+
IoIosArrowBackComponent,
|
|
4
|
+
IoIosArrowForwardComponent,
|
|
5
|
+
IoMdArrowBackComponent,
|
|
6
|
+
IoMdArrowForwardComponent,
|
|
7
|
+
} from '../icons';
|
|
8
|
+
|
|
9
|
+
// Mapeo dirección+arrowType -> icono:
|
|
10
|
+
// chevron `IoIos*` para arrowType 'arrow', flecha Material `IoMd*` para
|
|
11
|
+
// 'arrow-round', según dirección. Los componentes vienen del módulo de
|
|
12
|
+
// iconos inline dedicado (`../icons`, Task A6).
|
|
13
|
+
// DISCLAIMER: se usa un <span> en vez de <button>
|
|
14
|
+
// porque el elemento button no posiciona bien el icono de flecha.
|
|
15
|
+
@Component({
|
|
16
|
+
selector: 'ba-btn-arrow-dir',
|
|
17
|
+
standalone: true,
|
|
18
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
19
|
+
imports: [IoIosArrowBackComponent, IoIosArrowForwardComponent, IoMdArrowBackComponent, IoMdArrowForwardComponent],
|
|
20
|
+
template: `
|
|
21
|
+
<span
|
|
22
|
+
[class]="
|
|
23
|
+
'bal-ba-transition-2 bal-ba-outline-none bas-group bas-flex bas-items-center bas-justify-center bas-border-none ' +
|
|
24
|
+
bgtnSizeClassNames() + ' ' + (!disabled() ? btnClassNames() : btnDisabledClassNames())
|
|
25
|
+
"
|
|
26
|
+
[attr.data-testid]="'btn-arrow-' + direction()"
|
|
27
|
+
(click)="onBtnClicked()"
|
|
28
|
+
>
|
|
29
|
+
@if (arrowType() === 'arrow' && direction() === 'back') {
|
|
30
|
+
<ba-io-ios-arrow-back [class]="arrowClasses()" />
|
|
31
|
+
} @else if (arrowType() === 'arrow' && direction() === 'forward') {
|
|
32
|
+
<ba-io-ios-arrow-forward [class]="arrowClasses()" />
|
|
33
|
+
} @else if (arrowType() === 'arrow-round' && direction() === 'back') {
|
|
34
|
+
<ba-io-md-arrow-back [class]="arrowClasses()" />
|
|
35
|
+
} @else {
|
|
36
|
+
<ba-io-md-arrow-forward [class]="arrowClasses()" />
|
|
37
|
+
}
|
|
38
|
+
</span>
|
|
39
|
+
`,
|
|
40
|
+
})
|
|
41
|
+
export class BtnArrowDirComponent {
|
|
42
|
+
direction = input<'back' | 'forward'>('back');
|
|
43
|
+
disabled = input(false);
|
|
44
|
+
arrowType = input<'arrow' | 'arrow-round'>('arrow');
|
|
45
|
+
btnClassNames = input('bas-rounded-full bas-cursor-pointer bas-bg-[var(--bal-primary-10)] hover:bas-bg-[#e0edf5]');
|
|
46
|
+
btnDisabledClassNames = input('bas-rounded-full bas-cursor-not-allowed bas-bg-[var(--bal-primary-10)]');
|
|
47
|
+
bgtnSizeClassNames = input('bas-h-8 bas-w-8');
|
|
48
|
+
arrowClassNames = input('bas-text-[var(--bal-primary-600)] group-hover:bas-text-[var(--bal-primary-800)]');
|
|
49
|
+
arrowDisabledClassNames = input('bas-text-[var(--bal-primary-500)]');
|
|
50
|
+
arrowSizesClassNames = input('bas-h-6 bas-w-6');
|
|
51
|
+
|
|
52
|
+
clicked = output<void>();
|
|
53
|
+
|
|
54
|
+
arrowClasses = computed(
|
|
55
|
+
() =>
|
|
56
|
+
`bal-ba-transition-2 ${this.arrowSizesClassNames()} ${
|
|
57
|
+
!this.disabled() ? this.arrowClassNames() : this.arrowDisabledClassNames()
|
|
58
|
+
}`,
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
onBtnClicked(): void {
|
|
62
|
+
if (this.disabled()) return;
|
|
63
|
+
this.clicked.emit();
|
|
64
|
+
}
|
|
65
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from 'vitest';
|
|
2
|
+
import { Component } from '@angular/core';
|
|
3
|
+
import { render, screen, fireEvent } from '@testing-library/angular';
|
|
4
|
+
import { BtnBaseComponent } from './btn-base.component';
|
|
5
|
+
|
|
6
|
+
// Los signal inputs se fijan con binding de plantilla desde un host component
|
|
7
|
+
// (forma inequívoca con `input()`), en vez de `componentInputs` (que no aplica
|
|
8
|
+
// signal inputs de forma fiable en @testing-library/angular 18).
|
|
9
|
+
describe('BtnBaseComponent', () => {
|
|
10
|
+
it('renderiza el texto', async () => {
|
|
11
|
+
@Component({ standalone: true, imports: [BtnBaseComponent], template: `<ba-btn-base text="Entrar" />` })
|
|
12
|
+
class HostComponent {}
|
|
13
|
+
await render(HostComponent);
|
|
14
|
+
expect(screen.getByRole('button').textContent).toContain('Entrar');
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it('al hacer click emite action', async () => {
|
|
18
|
+
const onAction = vi.fn();
|
|
19
|
+
@Component({
|
|
20
|
+
standalone: true,
|
|
21
|
+
imports: [BtnBaseComponent],
|
|
22
|
+
template: `<ba-btn-base text="Entrar" (action)="onAction()" />`,
|
|
23
|
+
})
|
|
24
|
+
class HostComponent {
|
|
25
|
+
onAction = onAction;
|
|
26
|
+
}
|
|
27
|
+
await render(HostComponent);
|
|
28
|
+
await fireEvent.click(screen.getByRole('button'));
|
|
29
|
+
expect(onAction).toHaveBeenCalled();
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it('en modo loader no muestra cursor-pointer y monta el loader', async () => {
|
|
33
|
+
@Component({ standalone: true, imports: [BtnBaseComponent], template: `<ba-btn-base [isLoader]="true" />` })
|
|
34
|
+
class HostComponent {}
|
|
35
|
+
await render(HostComponent);
|
|
36
|
+
expect(screen.getByRole('button').className).toContain('bas-cursor-default');
|
|
37
|
+
});
|
|
38
|
+
});
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import { ChangeDetectionStrategy, Component, computed, input, output } from '@angular/core';
|
|
2
|
+
import { HorizontalLoaderComponent } from './horizontal-loader.component';
|
|
3
|
+
import { IconBaseComponent } from './icon-base.component';
|
|
4
|
+
|
|
5
|
+
// El JSX de React/Vue renderiza dos <button> distintos por `type`; aquí
|
|
6
|
+
// `btnClass()` computa la clase según `type()` con un único <button> — el
|
|
7
|
+
// DOM resultante es el mismo.
|
|
8
|
+
type BtnSize = 'xxs' | 'xs' | 'tiny' | 'small' | 'base' | 'large';
|
|
9
|
+
|
|
10
|
+
@Component({
|
|
11
|
+
selector: 'ba-btn-base',
|
|
12
|
+
standalone: true,
|
|
13
|
+
// El <button> interno lleva las clases de layout (p.ej. bas-w-full). El host
|
|
14
|
+
// debe ser transparente para que el <button> sea el hijo real del flex/grid
|
|
15
|
+
// del padre (como en React, donde BtnBase renderiza el <button> directamente).
|
|
16
|
+
// Sin esto, `bas-w-full` mide el 100% del host inline (que se encoge al
|
|
17
|
+
// contenido) y el botón "Sign Up" del flow no ocupa todo el ancho.
|
|
18
|
+
host: { style: 'display: contents' },
|
|
19
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
20
|
+
imports: [HorizontalLoaderComponent, IconBaseComponent],
|
|
21
|
+
template: `
|
|
22
|
+
<button
|
|
23
|
+
[id]="btnId()"
|
|
24
|
+
[type]="btnType()"
|
|
25
|
+
[disabled]="disabled()"
|
|
26
|
+
[class]="btnClass()"
|
|
27
|
+
(click)="onBtnClicked($event)"
|
|
28
|
+
>
|
|
29
|
+
@if (isLoader()) {
|
|
30
|
+
<ba-horizontal-loader
|
|
31
|
+
[loaderColor]="loaderColor()"
|
|
32
|
+
[loaderHeight]="loaderHeight()"
|
|
33
|
+
[loaderWidth]="loaderWidth()"
|
|
34
|
+
[classNames]="loaderContainerClassNames()"
|
|
35
|
+
/>
|
|
36
|
+
}
|
|
37
|
+
@if (isIcon()) {
|
|
38
|
+
<ba-icon-base
|
|
39
|
+
[iconUrl]="iconUrl()"
|
|
40
|
+
[iconWidth]="iconWidth()"
|
|
41
|
+
[iconHeight]="iconHeight()"
|
|
42
|
+
[iconClassNames]="iconClassNames()"
|
|
43
|
+
><ng-content select="[slot=icon]" /></ba-icon-base>
|
|
44
|
+
}
|
|
45
|
+
<span [class]="'bal-ba-transition-2 ' + realTextClassNames() + ' bas-flex bas-h-full bas-items-center'">
|
|
46
|
+
<ng-content />{{ text() }}
|
|
47
|
+
</span>
|
|
48
|
+
</button>
|
|
49
|
+
`,
|
|
50
|
+
})
|
|
51
|
+
export class BtnBaseComponent {
|
|
52
|
+
btnId = input('btn-base');
|
|
53
|
+
text = input('');
|
|
54
|
+
size = input<BtnSize>('base');
|
|
55
|
+
type = input<'button-outlined' | 'button-border'>('button-outlined');
|
|
56
|
+
btnType = input<'button' | 'submit' | 'reset'>('button');
|
|
57
|
+
ringClassNames = input('bal-ba-hover-ring-primary-200 bal-ba-focus-ring-primary-300');
|
|
58
|
+
offsetClassNames = input('bal-ba-focus-ring-offset-2 bal-ba-hover-ring-offset-2');
|
|
59
|
+
commonClassNames = input(`bal-ba-transition-2 bas-group bas-relative bas-box-border
|
|
60
|
+
bas-flex bas-flex-row bas-gap-2 bas-items-center bas-overflow-hidden`);
|
|
61
|
+
classNames = input('bas-justify-center');
|
|
62
|
+
bgClassNames = input<string | undefined>(undefined);
|
|
63
|
+
borderClassNames = input<string | undefined>(undefined);
|
|
64
|
+
paddingClassNames = input<string | undefined>(undefined);
|
|
65
|
+
textClassNames = input<string | undefined>(undefined);
|
|
66
|
+
isIcon = input(true);
|
|
67
|
+
iconUrl = input<string | undefined>(undefined);
|
|
68
|
+
iconWidth = input(24);
|
|
69
|
+
iconHeight = input(24);
|
|
70
|
+
iconClassNames = input('bas-rounded-full');
|
|
71
|
+
isLoader = input(false);
|
|
72
|
+
loaderColor = input('#003e79');
|
|
73
|
+
loaderWidth = input(80);
|
|
74
|
+
loaderContainerClassNames = input('bas-rounded-md');
|
|
75
|
+
isFocusOutlinedEnabled = input(true);
|
|
76
|
+
disabled = input(false);
|
|
77
|
+
applyDisabledStyles = input(true);
|
|
78
|
+
|
|
79
|
+
/** Equivalente al emit `click` de BtnBase.vue / onClick de BtnBase.tsx. */
|
|
80
|
+
action = output<void>();
|
|
81
|
+
|
|
82
|
+
onBtnClicked(e: MouseEvent): void {
|
|
83
|
+
e.preventDefault();
|
|
84
|
+
this.action.emit();
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
private realBorderClassNames = computed(
|
|
88
|
+
() =>
|
|
89
|
+
this.borderClassNames() ??
|
|
90
|
+
(this.type() === 'button-border'
|
|
91
|
+
? `bas-rounded-md bas-border-2 bas-border-solid bas-border-[var(--bal-primary-100)]
|
|
92
|
+
hover:bas-border-[var(--bal-primary-300)]`
|
|
93
|
+
: 'bas-border-none bas-rounded-md'),
|
|
94
|
+
);
|
|
95
|
+
realTextClassNames = computed(() => {
|
|
96
|
+
if (this.textClassNames()) return this.textClassNames()!;
|
|
97
|
+
const textBase = 'bas-text-sm bas-font-semibold bas-leading-6';
|
|
98
|
+
return this.type() === 'button-border'
|
|
99
|
+
? `${textBase} bas-text-[var(--bal-primary-700)] group-hover:bas-text-[var(--bal-primary-900)]`
|
|
100
|
+
: `${textBase} bas-text-white`;
|
|
101
|
+
});
|
|
102
|
+
private realBgClassNames = computed(
|
|
103
|
+
() =>
|
|
104
|
+
this.bgClassNames() ??
|
|
105
|
+
(this.type() === 'button-border'
|
|
106
|
+
? 'bas-bg-transparent'
|
|
107
|
+
: 'bas-bg-[var(--bal-primary-500)] hover:bas-bg-[var(--bal-primary-600)]'),
|
|
108
|
+
);
|
|
109
|
+
private sizeStyles = computed(
|
|
110
|
+
() =>
|
|
111
|
+
({
|
|
112
|
+
xxs: `bas-h-[34px] ${this.paddingClassNames() || 'bas-px-1 bas-py-0'}`,
|
|
113
|
+
xs: `bas-h-[36px] ${this.paddingClassNames() || 'bas-px-1.5 bas-py-0'}`,
|
|
114
|
+
tiny: `bas-h-[38px] ${this.paddingClassNames() || 'bas-px-2 bas-py-0'}`,
|
|
115
|
+
small: `bas-h-[42px] ${this.paddingClassNames() || 'bas-px-2.5 bas-py-0'}`,
|
|
116
|
+
base: `bas-h-[46px] ${this.paddingClassNames() || 'bas-px-3 bas-py-0'}`,
|
|
117
|
+
large: `bas-h-[54px] ${this.paddingClassNames() || 'bas-px-3.5 bas-py-0'}`,
|
|
118
|
+
})[this.size()],
|
|
119
|
+
);
|
|
120
|
+
loaderHeight = computed(
|
|
121
|
+
() =>
|
|
122
|
+
({ xxs: 34, xs: 36, tiny: 38, small: 42, base: 46, large: 54 }[this.size()] -
|
|
123
|
+
(this.type() === 'button-border' ? 4 : 0)),
|
|
124
|
+
);
|
|
125
|
+
|
|
126
|
+
btnClass = computed(() => {
|
|
127
|
+
if (this.type() === 'button-outlined') {
|
|
128
|
+
return `bal-ba-ring-base bal-ba-ring ${this.commonClassNames()} ${this.realBgClassNames()}
|
|
129
|
+
${this.ringClassNames()} ${this.sizeStyles()}
|
|
130
|
+
${this.applyDisabledStyles() ? 'disabled:bas-cursor-not-allowed disabled:bas-opacity-50' : ''}
|
|
131
|
+
${this.isLoader() ? 'bas-cursor-default' : 'bal-ba-hover-ring-2 bas-cursor-pointer'}
|
|
132
|
+
${this.isFocusOutlinedEnabled() && !this.isLoader() ? 'bal-ba-focus-ring-2 focus:bas-opacity-90' : ''}
|
|
133
|
+
${this.offsetClassNames()} ${this.classNames() || ''} ${this.realBorderClassNames()}`;
|
|
134
|
+
}
|
|
135
|
+
return `${this.commonClassNames()} ${this.sizeStyles()}
|
|
136
|
+
${this.isLoader() ? 'bas-cursor-default' : 'bas-cursor-pointer'}
|
|
137
|
+
${this.applyDisabledStyles() ? 'disabled:bas-cursor-not-allowed disabled:bas-opacity-50' : ''}
|
|
138
|
+
${this.realBorderClassNames()} ${this.realBgClassNames()} ${this.classNames() || ''}`;
|
|
139
|
+
});
|
|
140
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { Component } from '@angular/core';
|
|
3
|
+
import { render, screen } from '@testing-library/angular';
|
|
4
|
+
import { DividerBaseComponent } from './divider-base.component';
|
|
5
|
+
describe('DividerBaseComponent', () => {
|
|
6
|
+
it('proyecta el contenido central cuando lo hay', async () => {
|
|
7
|
+
@Component({ standalone: true, imports: [DividerBaseComponent], template: `<ba-divider-base><span data-testid="mid">or</span></ba-divider-base>` })
|
|
8
|
+
class Host {}
|
|
9
|
+
await render(Host);
|
|
10
|
+
expect(screen.getByTestId('mid')).toBeTruthy();
|
|
11
|
+
});
|
|
12
|
+
});
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import {
|
|
2
|
+
AfterViewChecked,
|
|
3
|
+
ChangeDetectionStrategy,
|
|
4
|
+
Component,
|
|
5
|
+
ElementRef,
|
|
6
|
+
input,
|
|
7
|
+
signal,
|
|
8
|
+
viewChild,
|
|
9
|
+
} from '@angular/core';
|
|
10
|
+
|
|
11
|
+
// `children` (ReactNode / slot por
|
|
12
|
+
// defecto) pasa a ser `<ng-content />`; la presencia de contenido proyectado
|
|
13
|
+
// se detecta igual que en IconBaseComponent (ver comentario allí).
|
|
14
|
+
@Component({
|
|
15
|
+
selector: 'ba-divider-base',
|
|
16
|
+
standalone: true,
|
|
17
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
18
|
+
template: `
|
|
19
|
+
<div
|
|
20
|
+
[class]="
|
|
21
|
+
'bas-group bas-relative bas-box-border bas-grid bas-w-full bas-items-center ' +
|
|
22
|
+
classNames() +
|
|
23
|
+
' ' +
|
|
24
|
+
zIndex() +
|
|
25
|
+
' ' +
|
|
26
|
+
(hasChildren() ? 'bas-grid-cols-[1fr_auto_1fr]' : 'bas-grid-cols-[1fr_1fr]')
|
|
27
|
+
"
|
|
28
|
+
>
|
|
29
|
+
<span
|
|
30
|
+
[class]="
|
|
31
|
+
'bas-border-solid bas-w-full bas-border-x-0 bas-border-b-0 ' +
|
|
32
|
+
borderSizeClassName() +
|
|
33
|
+
' ' +
|
|
34
|
+
(dividerBorderColorClassNames() || 'bas-border-gray-300')
|
|
35
|
+
"
|
|
36
|
+
></span>
|
|
37
|
+
<span
|
|
38
|
+
#childrenSlot
|
|
39
|
+
[style.display]="hasChildren() ? 'contents' : 'none'"
|
|
40
|
+
[class]="'bas-group ' + childrenContainerClassNames()"
|
|
41
|
+
>
|
|
42
|
+
<ng-content />
|
|
43
|
+
</span>
|
|
44
|
+
<span
|
|
45
|
+
[class]="
|
|
46
|
+
'bas-border-solid bas-w-full bas-border-x-0 bas-border-b-0 ' +
|
|
47
|
+
borderSizeClassName() +
|
|
48
|
+
' ' +
|
|
49
|
+
(dividerBorderColorClassNames() || 'bas-border-gray-300')
|
|
50
|
+
"
|
|
51
|
+
></span>
|
|
52
|
+
</div>
|
|
53
|
+
`,
|
|
54
|
+
})
|
|
55
|
+
export class DividerBaseComponent implements AfterViewChecked {
|
|
56
|
+
classNames = input('');
|
|
57
|
+
dividerBorderColorClassNames = input<string | undefined>(undefined);
|
|
58
|
+
borderSizeClassName = input('bas-border-t');
|
|
59
|
+
childrenContainerClassNames = input(`bas-flex bas-flex-row bas-items-center bas-text-sm
|
|
60
|
+
bas-justify-center bas-gap-1 bas-bg-transparent bas-px-2 bas-text-gray-500`);
|
|
61
|
+
zIndex = input('bas-z-50');
|
|
62
|
+
|
|
63
|
+
// Signal query (no `@ViewChild` — los decoradores de campo clásicos no se
|
|
64
|
+
// soportan en el modo JIT del harness de test AnalogJS/Vitest).
|
|
65
|
+
private readonly childrenSlot = viewChild<ElementRef<HTMLElement>>('childrenSlot');
|
|
66
|
+
|
|
67
|
+
hasChildren = signal(false);
|
|
68
|
+
|
|
69
|
+
ngAfterViewChecked(): void {
|
|
70
|
+
const el = this.childrenSlot()?.nativeElement ?? null;
|
|
71
|
+
const has = !!el && el.childNodes.length > 0;
|
|
72
|
+
if (has !== this.hasChildren()) this.hasChildren.set(has);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { Component } from '@angular/core';
|
|
3
|
+
import { render } from '@testing-library/angular';
|
|
4
|
+
import { DropdownArrowComponent } from './dropdown-arrow.component';
|
|
5
|
+
describe('DropdownArrowComponent', () => {
|
|
6
|
+
it('aplica la clase abierta cuando isOpen', async () => {
|
|
7
|
+
@Component({ standalone: true, imports: [DropdownArrowComponent], template: `<ba-dropdown-arrow [isOpen]="true" />` })
|
|
8
|
+
class Host {}
|
|
9
|
+
const { container } = await render(Host);
|
|
10
|
+
expect(container.querySelector('.bas-dropdown-arrow-open')).toBeTruthy();
|
|
11
|
+
});
|
|
12
|
+
});
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { ChangeDetectionStrategy, Component, input, output } from '@angular/core';
|
|
2
|
+
|
|
3
|
+
// La rotación animada de
|
|
4
|
+
// framer-motion (variants open/closed) pasa a `transition: transform` en los
|
|
5
|
+
// `styles` del componente con la clase `bas-dropdown-arrow-open` condicional.
|
|
6
|
+
@Component({
|
|
7
|
+
selector: 'ba-dropdown-arrow',
|
|
8
|
+
standalone: true,
|
|
9
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
10
|
+
template: `
|
|
11
|
+
<span
|
|
12
|
+
[id]="id()"
|
|
13
|
+
class="bas-dropdown-arrow bas-flex bas-h-full bas-w-full bas-items-center bas-justify-center"
|
|
14
|
+
[class.bas-dropdown-arrow-open]="isOpen()"
|
|
15
|
+
(click)="clicked.emit()"
|
|
16
|
+
>
|
|
17
|
+
<svg
|
|
18
|
+
[class]="(disabled() ? 'bas-cursor-not-allowed' : 'bas-cursor-pointer') + ' ' + sizeClassNames()"
|
|
19
|
+
viewBox="0 0 20 20"
|
|
20
|
+
>
|
|
21
|
+
<path d="M0 7 L 20 7 L 10 16" [attr.fill]="color()" />
|
|
22
|
+
</svg>
|
|
23
|
+
</span>
|
|
24
|
+
`,
|
|
25
|
+
styles: [
|
|
26
|
+
`
|
|
27
|
+
.bas-dropdown-arrow {
|
|
28
|
+
transform: rotate(0deg);
|
|
29
|
+
transition: transform 0.25s ease;
|
|
30
|
+
}
|
|
31
|
+
.bas-dropdown-arrow-open {
|
|
32
|
+
transform: rotate(180deg);
|
|
33
|
+
}
|
|
34
|
+
`,
|
|
35
|
+
],
|
|
36
|
+
})
|
|
37
|
+
export class DropdownArrowComponent {
|
|
38
|
+
id = input('animated-dropdown-id');
|
|
39
|
+
isOpen = input(false);
|
|
40
|
+
sizeClassNames = input('bas-h-4 bas-w-4');
|
|
41
|
+
color = input('var(--bal-primary-700)');
|
|
42
|
+
disabled = input(false);
|
|
43
|
+
|
|
44
|
+
/** Equivalente al emit `click` de DropdownArrow.vue. */
|
|
45
|
+
clicked = output<void>();
|
|
46
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from 'vitest';
|
|
2
|
+
import { Component } from '@angular/core';
|
|
3
|
+
import { render, screen, fireEvent } from '@testing-library/angular';
|
|
4
|
+
import { DropdownBaseComponent } from './dropdown-base.component';
|
|
5
|
+
|
|
6
|
+
describe('DropdownBaseComponent', () => {
|
|
7
|
+
it('al pulsar el boton emite isOpenChange con el valor invertido', async () => {
|
|
8
|
+
const onOpen = vi.fn();
|
|
9
|
+
@Component({ standalone: true, imports: [DropdownBaseComponent], template: `<ba-dropdown-base [isOpen]="false" (isOpenChange)="f($event)" />` })
|
|
10
|
+
class Host { f = onOpen; }
|
|
11
|
+
await render(Host);
|
|
12
|
+
await fireEvent.click(screen.getByRole('button'));
|
|
13
|
+
expect(onOpen).toHaveBeenCalledWith(true);
|
|
14
|
+
});
|
|
15
|
+
it('con isOpen muestra la lista y al elegir un item emite changeSelectedItem', async () => {
|
|
16
|
+
const onSel = vi.fn();
|
|
17
|
+
@Component({ standalone: true, imports: [DropdownBaseComponent], template: `<ba-dropdown-base [isOpen]="true" (changeSelectedItem)="f($event)" />` })
|
|
18
|
+
class Host { f = onSel; }
|
|
19
|
+
await render(Host);
|
|
20
|
+
await fireEvent.click(screen.getByText('Block-Auth'));
|
|
21
|
+
expect(onSel).toHaveBeenCalled();
|
|
22
|
+
expect(onSel.mock.calls[0][0]).toMatchObject({ value: 'blockauth' });
|
|
23
|
+
});
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
// Click-outside con container (semantica portada de DropdownBase.tsx).
|
|
27
|
+
describe('DropdownBaseComponent — click-outside', () => {
|
|
28
|
+
it('sin container ningun click emite clickOutside', async () => {
|
|
29
|
+
const onOut = vi.fn();
|
|
30
|
+
@Component({ standalone: true, imports: [DropdownBaseComponent], template: `<ba-dropdown-base [isOpen]="true" (clickOutside)="f()" />` })
|
|
31
|
+
class Host { f = onOut; }
|
|
32
|
+
await render(Host);
|
|
33
|
+
await fireEvent.click(document.body);
|
|
34
|
+
expect(onOut).not.toHaveBeenCalled();
|
|
35
|
+
});
|
|
36
|
+
it('click dentro de container pero fuera del dropdown emite clickOutside', async () => {
|
|
37
|
+
const onOut = vi.fn();
|
|
38
|
+
const outer = document.createElement('div');
|
|
39
|
+
document.body.appendChild(outer);
|
|
40
|
+
const sibling = document.createElement('div');
|
|
41
|
+
outer.appendChild(sibling);
|
|
42
|
+
@Component({ standalone: true, imports: [DropdownBaseComponent], template: `<ba-dropdown-base [isOpen]="true" [container]="c" (clickOutside)="f()" />` })
|
|
43
|
+
class Host { c = outer; f = onOut; }
|
|
44
|
+
await render(Host);
|
|
45
|
+
await fireEvent.click(sibling);
|
|
46
|
+
expect(onOut).toHaveBeenCalled();
|
|
47
|
+
document.body.removeChild(outer);
|
|
48
|
+
});
|
|
49
|
+
});
|