Compare commits
2 Commits
88c3a08a23
...
8089298383
| Author | SHA1 | Date | |
|---|---|---|---|
| 8089298383 | |||
| d51db00247 |
@@ -67,6 +67,7 @@ export interface CatalogItemVariant {
|
|||||||
event_date_ids?: number[];
|
event_date_ids?: number[];
|
||||||
event_dates?: string[];
|
event_dates?: string[];
|
||||||
maximum_addable_quantity?: number | null;
|
maximum_addable_quantity?: number | null;
|
||||||
|
unavailable_message?: string | null;
|
||||||
minimum_use_date?: string | null;
|
minimum_use_date?: string | null;
|
||||||
maximum_use_date?: string | null;
|
maximum_use_date?: string | null;
|
||||||
effective_minimum_use_date?: string | null;
|
effective_minimum_use_date?: string | null;
|
||||||
@@ -114,6 +115,7 @@ export interface CatalogFeaturedItemVariant {
|
|||||||
descripcion?: string | null;
|
descripcion?: string | null;
|
||||||
precio?: string;
|
precio?: string;
|
||||||
maximum_addable_quantity?: number | null;
|
maximum_addable_quantity?: number | null;
|
||||||
|
unavailable_message?: string | null;
|
||||||
values: Record<string, CatalogVariantValue>;
|
values: Record<string, CatalogVariantValue>;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -146,6 +148,7 @@ export interface CatalogFeaturedItem {
|
|||||||
precio: number | string;
|
precio: number | string;
|
||||||
image?: string | null;
|
image?: string | null;
|
||||||
maximum_addable_quantity?: number | null;
|
maximum_addable_quantity?: number | null;
|
||||||
|
unavailable_message?: string | null;
|
||||||
variants?: CatalogFeaturedItemVariant[];
|
variants?: CatalogFeaturedItemVariant[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -31,9 +31,16 @@
|
|||||||
<div
|
<div
|
||||||
class="product-column-with-image__body p-3 d-flex flex-column align-items-center text-center flex-grow-1"
|
class="product-column-with-image__body p-3 d-flex flex-column align-items-center text-center flex-grow-1"
|
||||||
>
|
>
|
||||||
<h3 class="product-column-with-image__title text-uppercase text-black mb-4">{{ title() }}</h3>
|
<h3 class="product-column-with-image__title text-uppercase text-black mb-4">
|
||||||
|
{{ title() }}
|
||||||
|
@if (unavailableMessage(); as message) {
|
||||||
|
<app-tooltip [message]="message" />
|
||||||
|
}
|
||||||
|
</h3>
|
||||||
|
|
||||||
<div class="product-column-with-image__prices d-flex align-items-center justify-content-center gap-2">
|
<div
|
||||||
|
class="product-column-with-image__prices d-flex align-items-center justify-content-center gap-2"
|
||||||
|
>
|
||||||
<span class="product-column-with-image__price-discounted text-primary">
|
<span class="product-column-with-image__price-discounted text-primary">
|
||||||
{{ formattedDiscountedPrice() }}
|
{{ formattedDiscountedPrice() }}
|
||||||
</span>
|
</span>
|
||||||
@@ -53,7 +60,12 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="product-column-with-image__action mt-auto w-100">
|
<div class="product-column-with-image__action mt-auto w-100">
|
||||||
<app-button variant="primary" class="w-100" (click)="buy.emit()">
|
<app-button
|
||||||
|
variant="primary"
|
||||||
|
class="w-100"
|
||||||
|
[disabled]="!!unavailableMessage()"
|
||||||
|
(click)="onBuy()"
|
||||||
|
>
|
||||||
{{ buttonText() }}
|
{{ buttonText() }}
|
||||||
</app-button>
|
</app-button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -2,10 +2,11 @@ import { NgOptimizedImage } from '@angular/common';
|
|||||||
import { ChangeDetectionStrategy, Component, computed, input, output } from '@angular/core';
|
import { ChangeDetectionStrategy, Component, computed, input, output } from '@angular/core';
|
||||||
|
|
||||||
import { ButtonComponent } from '../button/button.component';
|
import { ButtonComponent } from '../button/button.component';
|
||||||
|
import { TooltipComponent } from '../tooltip/tooltip.component';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-product-column-with-image',
|
selector: 'app-product-column-with-image',
|
||||||
imports: [ButtonComponent, NgOptimizedImage],
|
imports: [ButtonComponent, NgOptimizedImage, TooltipComponent],
|
||||||
templateUrl: './product-column-with-image.component.html',
|
templateUrl: './product-column-with-image.component.html',
|
||||||
styleUrl: './product-column-with-image.component.scss',
|
styleUrl: './product-column-with-image.component.scss',
|
||||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
@@ -18,9 +19,16 @@ export class ProductColumnWithImageComponent {
|
|||||||
readonly transferPrice = input<number | null>(null);
|
readonly transferPrice = input<number | null>(null);
|
||||||
readonly buttonText = input<string>('Comprar');
|
readonly buttonText = input<string>('Comprar');
|
||||||
readonly imagePriority = input<boolean>(false);
|
readonly imagePriority = input<boolean>(false);
|
||||||
|
readonly unavailableMessage = input<string | null>(null);
|
||||||
|
|
||||||
readonly buy = output<void>();
|
readonly buy = output<void>();
|
||||||
|
|
||||||
|
protected onBuy(): void {
|
||||||
|
if (this.unavailableMessage()) return;
|
||||||
|
|
||||||
|
this.buy.emit();
|
||||||
|
}
|
||||||
|
|
||||||
readonly discountedPrice = computed(() => {
|
readonly discountedPrice = computed(() => {
|
||||||
const original = this.originalPrice();
|
const original = this.originalPrice();
|
||||||
const discount = this.discount();
|
const discount = this.discount();
|
||||||
|
|||||||
@@ -13,6 +13,7 @@
|
|||||||
[description]="item.descripcion ?? ''"
|
[description]="item.descripcion ?? ''"
|
||||||
[price]="price(item)"
|
[price]="price(item)"
|
||||||
[maximumAddableQuantity]="item.maximum_addable_quantity ?? null"
|
[maximumAddableQuantity]="item.maximum_addable_quantity ?? null"
|
||||||
|
[unavailableMessage]="item.unavailable_message ?? null"
|
||||||
[variants]="item.variants ?? []"
|
[variants]="item.variants ?? []"
|
||||||
[saving]="savingProductIds().has(item.id)"
|
[saving]="savingProductIds().has(item.id)"
|
||||||
(buy)="emitRowBuy(item, $event)"
|
(buy)="emitRowBuy(item, $event)"
|
||||||
@@ -25,6 +26,7 @@
|
|||||||
[description]="item.descripcion ?? ''"
|
[description]="item.descripcion ?? ''"
|
||||||
[price]="price(item)"
|
[price]="price(item)"
|
||||||
[maximumAddableQuantity]="item.maximum_addable_quantity ?? null"
|
[maximumAddableQuantity]="item.maximum_addable_quantity ?? null"
|
||||||
|
[unavailableMessage]="item.unavailable_message ?? null"
|
||||||
[variants]="item.variants ?? []"
|
[variants]="item.variants ?? []"
|
||||||
[saving]="savingProductIds().has(item.id)"
|
[saving]="savingProductIds().has(item.id)"
|
||||||
(buy)="emitColumnBuy(item, $event)"
|
(buy)="emitColumnBuy(item, $event)"
|
||||||
@@ -38,7 +40,8 @@
|
|||||||
[description]="item.descripcion ?? ''"
|
[description]="item.descripcion ?? ''"
|
||||||
[price]="price(item)"
|
[price]="price(item)"
|
||||||
[imageUrl]="loadImages() ? (item.image ?? null) : null"
|
[imageUrl]="loadImages() ? (item.image ?? null) : null"
|
||||||
[disabled]="loading()"
|
[unavailableMessage]="item.unavailable_message ?? null"
|
||||||
|
[disabled]="loading() || !!item.unavailable_message"
|
||||||
(buy)="emitTicketBuy(item, $event)"
|
(buy)="emitTicketBuy(item, $event)"
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
@@ -47,6 +50,7 @@
|
|||||||
[imageUrl]="loadImages() ? (item.image ?? null) : null"
|
[imageUrl]="loadImages() ? (item.image ?? null) : null"
|
||||||
[title]="item.nombre"
|
[title]="item.nombre"
|
||||||
[originalPrice]="price(item)"
|
[originalPrice]="price(item)"
|
||||||
|
[unavailableMessage]="item.unavailable_message ?? null"
|
||||||
[imagePriority]="loadImages() && index < 4"
|
[imagePriority]="loadImages() && index < 4"
|
||||||
(buy)="emitProductDetailBuy(item)"
|
(buy)="emitProductDetailBuy(item)"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -3,6 +3,9 @@
|
|||||||
<div class="product-row-card__info d-flex flex-column justify-content-center flex-grow-1">
|
<div class="product-row-card__info d-flex flex-column justify-content-center flex-grow-1">
|
||||||
<h3 class="product-row-card__title text-uppercase mb-1 m-0">
|
<h3 class="product-row-card__title text-uppercase mb-1 m-0">
|
||||||
{{ title() }}
|
{{ title() }}
|
||||||
|
@if (effectiveUnavailableMessage(); as message) {
|
||||||
|
<app-tooltip [message]="message" />
|
||||||
|
}
|
||||||
</h3>
|
</h3>
|
||||||
@if (effectiveDescription()) {
|
@if (effectiveDescription()) {
|
||||||
<p class="product-row-card__description m-0 mt-1">
|
<p class="product-row-card__description m-0 mt-1">
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import {
|
|||||||
} from '@angular/core';
|
} from '@angular/core';
|
||||||
import { ButtonComponent } from '../button/button.component';
|
import { ButtonComponent } from '../button/button.component';
|
||||||
import { QuantitySelectorComponent } from '../quantity-selector/quantity-selector.component';
|
import { QuantitySelectorComponent } from '../quantity-selector/quantity-selector.component';
|
||||||
|
import { TooltipComponent } from '../tooltip/tooltip.component';
|
||||||
import {
|
import {
|
||||||
VariantSelectorComponent,
|
VariantSelectorComponent,
|
||||||
VariantSelectorVariant,
|
VariantSelectorVariant,
|
||||||
@@ -19,12 +20,13 @@ export interface Variant extends VariantSelectorVariant {
|
|||||||
descripcion?: string | null;
|
descripcion?: string | null;
|
||||||
precio?: string | number;
|
precio?: string | number;
|
||||||
maximum_addable_quantity?: number | null;
|
maximum_addable_quantity?: number | null;
|
||||||
|
unavailable_message?: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-product-row-card',
|
selector: 'app-product-row-card',
|
||||||
standalone: true,
|
standalone: true,
|
||||||
imports: [ButtonComponent, QuantitySelectorComponent, VariantSelectorComponent],
|
imports: [ButtonComponent, QuantitySelectorComponent, TooltipComponent, VariantSelectorComponent],
|
||||||
templateUrl: './product-row-card.component.html',
|
templateUrl: './product-row-card.component.html',
|
||||||
styleUrl: './product-row-card.component.scss',
|
styleUrl: './product-row-card.component.scss',
|
||||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
@@ -35,6 +37,7 @@ export class ProductRowCardComponent {
|
|||||||
readonly description = input<string>('');
|
readonly description = input<string>('');
|
||||||
readonly price = input<number>(0);
|
readonly price = input<number>(0);
|
||||||
readonly maximumAddableQuantity = input<number | null>(null);
|
readonly maximumAddableQuantity = input<number | null>(null);
|
||||||
|
readonly unavailableMessage = input<string | null>(null);
|
||||||
readonly variants = input<Variant[]>([]);
|
readonly variants = input<Variant[]>([]);
|
||||||
readonly saving = input(false);
|
readonly saving = input(false);
|
||||||
|
|
||||||
@@ -61,6 +64,13 @@ export class ProductRowCardComponent {
|
|||||||
() => this.selectedVariantData()?.maximum_addable_quantity ?? this.maximumAddableQuantity(),
|
() => this.selectedVariantData()?.maximum_addable_quantity ?? this.maximumAddableQuantity(),
|
||||||
);
|
);
|
||||||
protected readonly unavailable = computed(() => this.effectiveMaximum() === 0);
|
protected readonly unavailable = computed(() => this.effectiveMaximum() === 0);
|
||||||
|
protected readonly effectiveUnavailableMessage = computed(() => {
|
||||||
|
const selectedVariant = this.selectedVariantData();
|
||||||
|
|
||||||
|
return selectedVariant
|
||||||
|
? (selectedVariant.unavailable_message ?? null)
|
||||||
|
: this.unavailableMessage();
|
||||||
|
});
|
||||||
|
|
||||||
readonly formattedPrice = computed(() => this.formatCurrency(this.effectivePrice()));
|
readonly formattedPrice = computed(() => this.formatCurrency(this.effectivePrice()));
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,12 @@
|
|||||||
<article class="ticket-selector">
|
<article class="ticket-selector">
|
||||||
<header class="ticket-selector__header">
|
<header class="ticket-selector__header">
|
||||||
<div>
|
<div>
|
||||||
<h3 class="ticket-selector__title">{{ title() }}</h3>
|
<h3 class="ticket-selector__title">
|
||||||
|
{{ title() }}
|
||||||
|
@if (unavailableMessage(); as message) {
|
||||||
|
<app-tooltip [message]="message" />
|
||||||
|
}
|
||||||
|
</h3>
|
||||||
@if (description()) {
|
@if (description()) {
|
||||||
<p class="ticket-selector__description">{{ description() }}</p>
|
<p class="ticket-selector__description">{{ description() }}</p>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ import { ModalService } from '../../../core/services/modal.service';
|
|||||||
import { ToastService } from '../../../core/services/toast.service';
|
import { ToastService } from '../../../core/services/toast.service';
|
||||||
import { ButtonComponent } from '../button/button.component';
|
import { ButtonComponent } from '../button/button.component';
|
||||||
import { IconButtonComponent } from '../icon-button/icon-button.component';
|
import { IconButtonComponent } from '../icon-button/icon-button.component';
|
||||||
|
import { TooltipComponent } from '../tooltip/tooltip.component';
|
||||||
|
|
||||||
type TicketSelectionStatus =
|
type TicketSelectionStatus =
|
||||||
| 'selecting'
|
| 'selecting'
|
||||||
@@ -56,7 +57,7 @@ interface TicketSelectionRow {
|
|||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-product-ticket-selector',
|
selector: 'app-product-ticket-selector',
|
||||||
imports: [ButtonComponent, IconButtonComponent],
|
imports: [ButtonComponent, IconButtonComponent, TooltipComponent],
|
||||||
templateUrl: './product-ticket-selector.component.html',
|
templateUrl: './product-ticket-selector.component.html',
|
||||||
styleUrl: './product-ticket-selector.component.scss',
|
styleUrl: './product-ticket-selector.component.scss',
|
||||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
@@ -80,6 +81,7 @@ export class ProductTicketSelectorComponent {
|
|||||||
readonly price = input<number>(0);
|
readonly price = input<number>(0);
|
||||||
readonly imageUrl = input<string | null>(null);
|
readonly imageUrl = input<string | null>(null);
|
||||||
readonly disabled = input(false);
|
readonly disabled = input(false);
|
||||||
|
readonly unavailableMessage = input<string | null>(null);
|
||||||
|
|
||||||
readonly buy = output<number[]>();
|
readonly buy = output<number[]>();
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,11 @@
|
|||||||
<article class="product-vertical-with-cart-card">
|
<article class="product-vertical-with-cart-card">
|
||||||
<div class="product-vertical-with-cart-card__content">
|
<div class="product-vertical-with-cart-card__content">
|
||||||
<h3 class="product-vertical-with-cart-card__title">{{ title() }}</h3>
|
<h3 class="product-vertical-with-cart-card__title">
|
||||||
|
{{ title() }}
|
||||||
|
@if (effectiveUnavailableMessage(); as message) {
|
||||||
|
<app-tooltip [message]="message" />
|
||||||
|
}
|
||||||
|
</h3>
|
||||||
|
|
||||||
@if (effectiveDescription()) {
|
@if (effectiveDescription()) {
|
||||||
<p class="product-vertical-with-cart-card__description">{{ effectiveDescription() }}</p>
|
<p class="product-vertical-with-cart-card__description">{{ effectiveDescription() }}</p>
|
||||||
|
|||||||
@@ -56,6 +56,23 @@ describe('ProductVerticalWithCartCardComponent', () => {
|
|||||||
).toBeNull();
|
).toBeNull();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('shows the backend availability message with the reusable tooltip', async () => {
|
||||||
|
const fixture = await createComponent();
|
||||||
|
fixture.componentRef.setInput('maximumAddableQuantity', 0);
|
||||||
|
fixture.componentRef.setInput(
|
||||||
|
'unavailableMessage',
|
||||||
|
'Alcanzaste el cupo máximo permitido para este producto.',
|
||||||
|
);
|
||||||
|
fixture.detectChanges();
|
||||||
|
|
||||||
|
const tooltip = (fixture.nativeElement as HTMLElement).querySelector('app-tooltip');
|
||||||
|
|
||||||
|
expect(tooltip?.querySelector('i.fa-solid.fa-circle-info')).not.toBeNull();
|
||||||
|
expect(tooltip?.textContent).toContain(
|
||||||
|
'Alcanzaste el cupo máximo permitido para este producto.',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
it('updates the quantity with the reusable quantity selector', async () => {
|
it('updates the quantity with the reusable quantity selector', async () => {
|
||||||
const fixture = await createComponent();
|
const fixture = await createComponent();
|
||||||
const buttons = fixture.nativeElement.querySelectorAll(
|
const buttons = fixture.nativeElement.querySelectorAll(
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import {
|
|||||||
|
|
||||||
import { ButtonComponent } from '../button/button.component';
|
import { ButtonComponent } from '../button/button.component';
|
||||||
import { QuantitySelectorComponent } from '../quantity-selector/quantity-selector.component';
|
import { QuantitySelectorComponent } from '../quantity-selector/quantity-selector.component';
|
||||||
|
import { TooltipComponent } from '../tooltip/tooltip.component';
|
||||||
import {
|
import {
|
||||||
VariantSelectorComponent,
|
VariantSelectorComponent,
|
||||||
VariantSelectorVariant,
|
VariantSelectorVariant,
|
||||||
@@ -19,11 +20,12 @@ export interface VerticalCartVariant extends VariantSelectorVariant {
|
|||||||
descripcion?: string | null;
|
descripcion?: string | null;
|
||||||
precio?: string | number;
|
precio?: string | number;
|
||||||
maximum_addable_quantity?: number | null;
|
maximum_addable_quantity?: number | null;
|
||||||
|
unavailable_message?: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-product-vertical-with-cart-card',
|
selector: 'app-product-vertical-with-cart-card',
|
||||||
imports: [ButtonComponent, QuantitySelectorComponent, VariantSelectorComponent],
|
imports: [ButtonComponent, QuantitySelectorComponent, TooltipComponent, VariantSelectorComponent],
|
||||||
templateUrl: './product-vertical-with-cart-card.component.html',
|
templateUrl: './product-vertical-with-cart-card.component.html',
|
||||||
styleUrl: './product-vertical-with-cart-card.component.scss',
|
styleUrl: './product-vertical-with-cart-card.component.scss',
|
||||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
@@ -33,6 +35,7 @@ export class ProductVerticalWithCartCardComponent {
|
|||||||
readonly description = input<string>('');
|
readonly description = input<string>('');
|
||||||
readonly price = input<number>(0);
|
readonly price = input<number>(0);
|
||||||
readonly maximumAddableQuantity = input<number | null>(null);
|
readonly maximumAddableQuantity = input<number | null>(null);
|
||||||
|
readonly unavailableMessage = input<string | null>(null);
|
||||||
readonly variants = input<VerticalCartVariant[]>([]);
|
readonly variants = input<VerticalCartVariant[]>([]);
|
||||||
readonly saving = input(false);
|
readonly saving = input(false);
|
||||||
|
|
||||||
@@ -59,6 +62,13 @@ export class ProductVerticalWithCartCardComponent {
|
|||||||
() => this.selectedVariantData()?.maximum_addable_quantity ?? this.maximumAddableQuantity(),
|
() => this.selectedVariantData()?.maximum_addable_quantity ?? this.maximumAddableQuantity(),
|
||||||
);
|
);
|
||||||
protected readonly unavailable = computed(() => this.effectiveMaximum() === 0);
|
protected readonly unavailable = computed(() => this.effectiveMaximum() === 0);
|
||||||
|
protected readonly effectiveUnavailableMessage = computed(() => {
|
||||||
|
const selectedVariant = this.selectedVariantData();
|
||||||
|
|
||||||
|
return selectedVariant
|
||||||
|
? (selectedVariant.unavailable_message ?? null)
|
||||||
|
: this.unavailableMessage();
|
||||||
|
});
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
effect(() => {
|
effect(() => {
|
||||||
|
|||||||
11
src/app/shared/components/tooltip/tooltip.component.html
Normal file
11
src/app/shared/components/tooltip/tooltip.component.html
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="tooltip-trigger"
|
||||||
|
[attr.aria-describedby]="tooltipId"
|
||||||
|
[attr.aria-label]="message()"
|
||||||
|
>
|
||||||
|
<i class="fa-solid fa-circle-info" aria-hidden="true"></i>
|
||||||
|
<span class="tooltip-message" [id]="tooltipId" role="tooltip">
|
||||||
|
{{ message() }}
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
62
src/app/shared/components/tooltip/tooltip.component.scss
Normal file
62
src/app/shared/components/tooltip/tooltip.component.scss
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
:host {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
margin-left: 0.25rem;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tooltip-trigger {
|
||||||
|
position: relative;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 0;
|
||||||
|
border: 0;
|
||||||
|
background: transparent;
|
||||||
|
color: var(--tenant-primary, var(--color-primary, #0d6efd));
|
||||||
|
font-size: 1em;
|
||||||
|
line-height: inherit;
|
||||||
|
|
||||||
|
&:focus-visible {
|
||||||
|
border-radius: 50%;
|
||||||
|
outline: 2px solid currentColor;
|
||||||
|
outline-offset: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover .tooltip-message,
|
||||||
|
&:focus-visible .tooltip-message {
|
||||||
|
visibility: visible;
|
||||||
|
opacity: 1;
|
||||||
|
transform: translate(-50%, -0.25rem);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.tooltip-message {
|
||||||
|
position: absolute;
|
||||||
|
z-index: 1100;
|
||||||
|
bottom: calc(100% + 0.625rem);
|
||||||
|
left: 50%;
|
||||||
|
width: max-content;
|
||||||
|
max-width: min(16rem, 75vw);
|
||||||
|
padding: 0.5rem 0.75rem;
|
||||||
|
border-radius: 0.375rem;
|
||||||
|
border: 1px solid var(--tenant-primary, var(--color-primary, #0d6efd));
|
||||||
|
background: #fff;
|
||||||
|
color: #212529;
|
||||||
|
font-family: inherit;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
font-weight: 400;
|
||||||
|
line-height: 1.35;
|
||||||
|
text-align: center;
|
||||||
|
text-transform: none;
|
||||||
|
letter-spacing: normal;
|
||||||
|
white-space: normal;
|
||||||
|
visibility: hidden;
|
||||||
|
opacity: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
transform: translate(-50%, 0);
|
||||||
|
transition:
|
||||||
|
opacity 0.15s ease,
|
||||||
|
transform 0.15s ease,
|
||||||
|
visibility 0.15s ease;
|
||||||
|
}
|
||||||
32
src/app/shared/components/tooltip/tooltip.component.spec.ts
Normal file
32
src/app/shared/components/tooltip/tooltip.component.spec.ts
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
import { TestBed, getTestBed } from '@angular/core/testing';
|
||||||
|
import { BrowserTestingModule, platformBrowserTesting } from '@angular/platform-browser/testing';
|
||||||
|
import { afterEach, beforeAll, describe, expect, it } from 'vitest';
|
||||||
|
|
||||||
|
import { TooltipComponent } from './tooltip.component';
|
||||||
|
|
||||||
|
describe('TooltipComponent', () => {
|
||||||
|
beforeAll(() => {
|
||||||
|
try {
|
||||||
|
getTestBed().initTestEnvironment(BrowserTestingModule, platformBrowserTesting());
|
||||||
|
} catch {
|
||||||
|
// Test environment may already be initialized by another setup entrypoint.
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => TestBed.resetTestingModule());
|
||||||
|
|
||||||
|
it('renders the reusable danger information tooltip', async () => {
|
||||||
|
await TestBed.configureTestingModule({ imports: [TooltipComponent] }).compileComponents();
|
||||||
|
const fixture = TestBed.createComponent(TooltipComponent);
|
||||||
|
fixture.componentRef.setInput('message', 'Este producto no tiene stock disponible.');
|
||||||
|
fixture.detectChanges();
|
||||||
|
|
||||||
|
const element = fixture.nativeElement as HTMLElement;
|
||||||
|
const trigger = element.querySelector('.tooltip-trigger');
|
||||||
|
const message = element.querySelector('.tooltip-message');
|
||||||
|
|
||||||
|
expect(trigger?.querySelector('i.fa-solid.fa-circle-info')).not.toBeNull();
|
||||||
|
expect(message?.textContent?.trim()).toBe('Este producto no tiene stock disponible.');
|
||||||
|
expect(trigger?.getAttribute('aria-describedby')).toBe(message?.id);
|
||||||
|
});
|
||||||
|
});
|
||||||
14
src/app/shared/components/tooltip/tooltip.component.ts
Normal file
14
src/app/shared/components/tooltip/tooltip.component.ts
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import { ChangeDetectionStrategy, Component, input } from '@angular/core';
|
||||||
|
|
||||||
|
let nextTooltipId = 0;
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-tooltip',
|
||||||
|
templateUrl: './tooltip.component.html',
|
||||||
|
styleUrl: './tooltip.component.scss',
|
||||||
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
|
})
|
||||||
|
export class TooltipComponent {
|
||||||
|
readonly message = input.required<string>();
|
||||||
|
protected readonly tooltipId = `app-tooltip-${nextTooltipId++}`;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user