diff --git a/projects/demo/src/modules/components/card-large/examples/14/index.html b/projects/demo/src/modules/components/card-large/examples/14/index.html new file mode 100644 index 000000000000..99a016446555 --- /dev/null +++ b/projects/demo/src/modules/components/card-large/examples/14/index.html @@ -0,0 +1,67 @@ + +
+ +

+ Leave your feedback + It will only take 3 minutes +

+ +
+ +

Why so?

+ + Leave a few words + + +
+ + +
diff --git a/projects/demo/src/modules/components/card-large/examples/14/index.less b/projects/demo/src/modules/components/card-large/examples/14/index.less new file mode 100644 index 000000000000..cb83f2063e20 --- /dev/null +++ b/projects/demo/src/modules/components/card-large/examples/14/index.less @@ -0,0 +1,18 @@ +.popover { + position: fixed; + right: 1.5rem; + bottom: 1.5rem; + inline-size: 20rem; +} + +.close { + position: absolute; + top: 0.5rem; + right: 0.75rem; +} + +.footer { + display: flex; + justify-content: flex-end; + gap: 0.5rem; +} diff --git a/projects/demo/src/modules/components/card-large/examples/14/index.ts b/projects/demo/src/modules/components/card-large/examples/14/index.ts new file mode 100644 index 000000000000..712c7a3ec17b --- /dev/null +++ b/projects/demo/src/modules/components/card-large/examples/14/index.ts @@ -0,0 +1,42 @@ +import {NgIf} from '@angular/common'; +import {Component, signal} from '@angular/core'; +import {FormsModule} from '@angular/forms'; +import {changeDetection} from '@demo/emulate/change-detection'; +import {encapsulation} from '@demo/emulate/encapsulation'; +import {TuiAutoFocus} from '@taiga-ui/cdk'; +import {TuiAppearance, TuiButton, TuiPopup, TuiTitle} from '@taiga-ui/core'; +import {TuiRating} from '@taiga-ui/kit'; +import {TuiCardLarge} from '@taiga-ui/layout'; +import {TuiTextareaModule, TuiTextfieldControllerModule} from '@taiga-ui/legacy'; + +@Component({ + standalone: true, + imports: [ + FormsModule, + NgIf, + TuiAppearance, + TuiAutoFocus, + TuiButton, + TuiCardLarge, + TuiPopup, + TuiRating, + TuiTextareaModule, + TuiTextfieldControllerModule, + TuiTitle, + ], + templateUrl: './index.html', + styleUrls: ['./index.less'], + encapsulation, + changeDetection, +}) +export default class Example { + protected readonly step = signal(0); + protected rating = 0; + protected comment = ''; + + protected close(): void { + this.rating = 0; + this.comment = ''; + this.step.set(0); + } +} diff --git a/projects/demo/src/modules/components/card-large/index.html b/projects/demo/src/modules/components/card-large/index.html index 59ae6a8dea64..4f26feb158f3 100644 --- a/projects/demo/src/modules/components/card-large/index.html +++ b/projects/demo/src/modules/components/card-large/index.html @@ -8,8 +8,8 @@ diff --git a/projects/demo/src/modules/components/card-large/index.ts b/projects/demo/src/modules/components/card-large/index.ts index 3609d242bb92..0ed0df9229f4 100644 --- a/projects/demo/src/modules/components/card-large/index.ts +++ b/projects/demo/src/modules/components/card-large/index.ts @@ -23,5 +23,6 @@ export default class Example { 'Image-dark', 'Paddings and radii', 'Map', + 'In portal', ]; } diff --git a/projects/demo/src/modules/components/pulse/examples/2/index.html b/projects/demo/src/modules/components/pulse/examples/2/index.html new file mode 100644 index 000000000000..362ac08803c8 --- /dev/null +++ b/projects/demo/src/modules/components/pulse/examples/2/index.html @@ -0,0 +1,103 @@ + + + + +

+ You can have images! +
Or any content really
+

+ Alex Inkin +
+ + +
+
+
+
+ +

+ Welcome to the tutorial! +
This is the first step.
+

+ + + + diff --git a/projects/demo/src/modules/components/pulse/examples/2/index.less b/projects/demo/src/modules/components/pulse/examples/2/index.less new file mode 100644 index 000000000000..632a48e6f81d --- /dev/null +++ b/projects/demo/src/modules/components/pulse/examples/2/index.less @@ -0,0 +1,25 @@ +:host { + display: flex; + flex-direction: column; + align-items: center; + gap: 1rem; +} + +.step-1 { + position: absolute; + top: 20%; + right: 10%; +} + +.step-2 { + position: absolute; + top: 25%; + right: 25%; +} + +.avatar { + inline-size: 100%; + block-size: 12rem; + object-fit: cover; + object-position: top; +} diff --git a/projects/demo/src/modules/components/pulse/examples/2/index.ts b/projects/demo/src/modules/components/pulse/examples/2/index.ts new file mode 100644 index 000000000000..b6a197eb77d1 --- /dev/null +++ b/projects/demo/src/modules/components/pulse/examples/2/index.ts @@ -0,0 +1,20 @@ +import {NgIf} from '@angular/common'; +import {Component, signal} from '@angular/core'; +import {changeDetection} from '@demo/emulate/change-detection'; +import {encapsulation} from '@demo/emulate/encapsulation'; +import {TuiButton, TuiHint, TuiTitle} from '@taiga-ui/core'; +import {tuiProvideExperimentalHint} from '@taiga-ui/experimental'; +import {TuiAvatar, TuiPulse} from '@taiga-ui/kit'; + +@Component({ + standalone: true, + imports: [NgIf, TuiAvatar, TuiButton, TuiHint, TuiPulse, TuiTitle], + templateUrl: './index.html', + styleUrls: ['./index.less'], + encapsulation, + changeDetection, + providers: [tuiProvideExperimentalHint()], +}) +export default class Example { + protected readonly step = signal(0); +} diff --git a/projects/demo/src/modules/components/pulse/index.html b/projects/demo/src/modules/components/pulse/index.html index 1999b2484986..f6070482f2a5 100644 --- a/projects/demo/src/modules/components/pulse/index.html +++ b/projects/demo/src/modules/components/pulse/index.html @@ -5,10 +5,11 @@ > diff --git a/projects/demo/src/modules/components/pulse/index.ts b/projects/demo/src/modules/components/pulse/index.ts index a6af1afa2f68..9a3726678d4a 100644 --- a/projects/demo/src/modules/components/pulse/index.ts +++ b/projects/demo/src/modules/components/pulse/index.ts @@ -8,4 +8,6 @@ import {TuiDemo} from '@demo/utils'; templateUrl: './index.html', changeDetection, }) -export default class Page {} +export default class Page { + protected readonly examples = ['Basic', 'Popover']; +} diff --git a/projects/demo/src/modules/components/push/examples/3/index.ts b/projects/demo/src/modules/components/push/examples/3/index.ts index 5c011e8e350f..a5065f00bf87 100644 --- a/projects/demo/src/modules/components/push/examples/3/index.ts +++ b/projects/demo/src/modules/components/push/examples/3/index.ts @@ -1,12 +1,12 @@ import {Component} from '@angular/core'; import {changeDetection} from '@demo/emulate/change-detection'; import {encapsulation} from '@demo/emulate/encapsulation'; -import {TuiButton, TuiIcon} from '@taiga-ui/core'; -import {TuiPush, TuiPushDirective} from '@taiga-ui/kit'; +import {TuiButton, TuiIcon, TuiLink} from '@taiga-ui/core'; +import {TuiPush} from '@taiga-ui/kit'; @Component({ standalone: true, - imports: [TuiButton, TuiIcon, TuiPush, TuiPushDirective], + imports: [TuiButton, TuiIcon, TuiLink, TuiPush], templateUrl: './index.html', encapsulation, changeDetection, diff --git a/projects/demo/src/modules/components/push/index.html b/projects/demo/src/modules/components/push/index.html index a913aedcc0c0..19219fa90b1d 100644 --- a/projects/demo/src/modules/components/push/index.html +++ b/projects/demo/src/modules/components/push/index.html @@ -7,24 +7,11 @@

Notifications in style of native browser push

- - - - diff --git a/projects/demo/src/modules/components/push/index.ts b/projects/demo/src/modules/components/push/index.ts index 20a720f87bd7..ebcde2176b73 100644 --- a/projects/demo/src/modules/components/push/index.ts +++ b/projects/demo/src/modules/components/push/index.ts @@ -14,6 +14,7 @@ import {TuiPush} from '@taiga-ui/kit'; changeDetection, }) export default class Page { + protected readonly examples = ['Basic', 'Service', 'Directive']; protected heading = ''; protected type = ''; diff --git a/projects/experimental/components/hint/hint.component.ts b/projects/experimental/components/hint/hint.component.ts new file mode 100644 index 000000000000..c8d788546ebf --- /dev/null +++ b/projects/experimental/components/hint/hint.component.ts @@ -0,0 +1,151 @@ +import type {Provider} from '@angular/core'; +import {ChangeDetectionStrategy, Component, inject, signal} from '@angular/core'; +import {takeUntilDestroyed} from '@angular/core/rxjs-interop'; +import {EMPTY_CLIENT_RECT} from '@taiga-ui/cdk/constants'; +import {TuiHoveredService} from '@taiga-ui/cdk/directives/hovered'; +import {TUI_IS_MOBILE} from '@taiga-ui/cdk/tokens'; +import type {TuiContext} from '@taiga-ui/cdk/types'; +import {tuiInjectElement} from '@taiga-ui/cdk/utils/dom'; +import {tuiClamp} from '@taiga-ui/cdk/utils/math'; +import {tuiPure, tuiPx} from '@taiga-ui/cdk/utils/miscellaneous'; +import {tuiFadeIn, tuiScaleIn} from '@taiga-ui/core/animations'; +import {TuiRectAccessor} from '@taiga-ui/core/classes'; +import {tuiButtonOptionsProvider} from '@taiga-ui/core/components/button'; +import {TuiAppearance, tuiAppearance} from '@taiga-ui/core/directives/appearance'; +import type {TuiHintDirective} from '@taiga-ui/core/directives/hint'; +import { + TUI_HINT_COMPONENT, + TUI_HINT_PROVIDERS, + TuiHintHover, + TuiHintPointer, + TuiHintUnstyledComponent, +} from '@taiga-ui/core/directives/hint'; +import {TuiPositionService, TuiVisualViewportService} from '@taiga-ui/core/services'; +import {TUI_ANIMATIONS_SPEED, TUI_VIEWPORT} from '@taiga-ui/core/tokens'; +import {tuiIsObscured, tuiToAnimationOptions} from '@taiga-ui/core/utils'; +import {injectContext, PolymorpheusOutlet} from '@taiga-ui/polymorpheus'; +import {map, takeWhile} from 'rxjs'; + +const GAP = 8; + +@Component({ + standalone: true, + selector: 'tui-hint', + imports: [PolymorpheusOutlet], + template: ` + + + `, + styleUrls: ['./hint.style.less'], + changeDetection: ChangeDetectionStrategy.OnPush, + providers: [TUI_HINT_PROVIDERS, tuiButtonOptionsProvider({size: 's'})], + animations: [tuiFadeIn, tuiScaleIn], + hostDirectives: [TuiAppearance], + host: { + '[@tuiScaleIn]': 'isMobile ? options : dummy', + '[@tuiFadeIn]': 'options', + '[class._untouchable]': 'pointer', + '[class._mobile]': 'isMobile', + '[attr.tuiTheme]': 'theme', + '(document:click)': 'onClick($event.target)', + }, +}) +export class TuiHintComponent { + private readonly el = tuiInjectElement(); + private readonly hover = inject(TuiHintHover); + private readonly vvs = inject(TuiVisualViewportService); + private readonly viewport = inject(TUI_VIEWPORT); + + protected readonly dummy = {value: '', params: {end: 1, start: 1}}; + protected readonly options = tuiToAnimationOptions( + inject(TUI_ANIMATIONS_SPEED), + 'cubic-bezier(0.35, 1.3, 0.25, 1)', + ); + + protected readonly pointer = inject(TuiHintPointer, {optional: true}); + protected readonly accessor = inject(TuiRectAccessor); + protected readonly hint = injectContext>>().$implicit; + protected readonly isMobile = inject(TUI_IS_MOBILE); + + protected readonly content = + this.hint.component.component === TuiHintUnstyledComponent + ? signal('') + : this.hint.content; + + protected readonly theme = this.hint.el + .closest('[tuiTheme]') + ?.getAttribute('tuiTheme'); + + protected readonly appearance = tuiAppearance(this.hint.appearance); + + constructor() { + inject(TuiPositionService) + .pipe( + takeWhile(() => this.hint.el.isConnected), + map((point) => this.vvs.correct(point)), + takeUntilDestroyed(), + ) + .subscribe({ + next: ([top, left]) => this.update(top, left), + complete: () => this.hover.toggle(false), + }); + + inject(TuiHoveredService) + .pipe(takeUntilDestroyed()) + .subscribe((hover) => this.hover.toggle(hover)); + } + + protected onClick(target: HTMLElement): void { + if ( + (!target.closest('tui-hint') && !this.hint.el.contains(target)) || + tuiIsObscured(this.hint.el) + ) { + this.hover.toggle(false); + } + } + + @tuiPure + private apply(top: string, left: string, beakTop: number, beakLeft: number): void { + this.el.style.top = top; + this.el.style.left = left; + this.el.style.setProperty('--top', `${beakTop}%`); + this.el.style.setProperty('--left', `${beakLeft}%`); + this.el.style.setProperty( + '--rotate', + !beakLeft || Math.ceil(beakLeft) === 100 ? '90deg' : '0deg', + ); + } + + private update(top: number, left: number): void { + const {clientHeight, clientWidth} = this.el; + const rect = this.accessor.getClientRect(); + const viewport = this.viewport.getClientRect(); + + if (rect === EMPTY_CLIENT_RECT || !clientHeight || !clientWidth) { + return; + } + + const safeLeft = tuiClamp(left, GAP, viewport.width - clientWidth - GAP); + const [beakTop, beakLeft] = this.vvs.correct([ + rect.top + rect.height / 2 - top, + rect.left + rect.width / 2 - safeLeft, + ]); + + this.apply( + tuiPx(Math.round(top)), + tuiPx(Math.round(safeLeft)), + Math.round((tuiClamp(beakTop, 0, clientHeight) / clientHeight) * 100), + Math.round((tuiClamp(beakLeft, 0, clientWidth) / clientWidth) * 100), + ); + } +} + +export function tuiProvideExperimentalHint(): Provider { + return { + provide: TUI_HINT_COMPONENT, + useValue: TuiHintComponent, + }; +} diff --git a/projects/experimental/components/hint/hint.style.less b/projects/experimental/components/hint/hint.style.less new file mode 100644 index 000000000000..ec30b55d62c7 --- /dev/null +++ b/projects/experimental/components/hint/hint.style.less @@ -0,0 +1,77 @@ +@import '@taiga-ui/core/styles/taiga-ui-local'; + +:host { + position: absolute; + max-inline-size: 20rem; + padding: 0.75rem 1rem; + background: var(--tui-background-accent-1); + border-radius: var(--tui-radius-l); + color: var(--tui-text-primary-on-accent-1); + box-sizing: border-box; + font: var(--tui-font-text-s); + white-space: pre-line; + overflow-wrap: break-word; + transform-origin: var(--left) var(--top); + + &::before { + content: ''; + position: absolute; + top: var(--top); + left: var(--left); + inline-size: 0.75rem; + block-size: 0.5rem; + background: inherit; + mask-image: url('data:image/svg+xml,'); + transform: translate(-50%, -50%) rotate(var(--rotate)); + } + + &._mobile { + font: var(--tui-font-text-m); + + &::before { + inline-size: 1.5rem; + block-size: 1.125rem; + mask-image: url('data:image/svg+xml,'); + } + } + + &:not([style*='top']) { + visibility: hidden; + } + + &._untouchable { + pointer-events: none; + } + + & ::ng-deep > { + [tuiTitle] { + margin-block-end: 0.75rem; + + [tuiSubtitle] { + color: var(--tui-text-secondary); + } + + + footer { + margin-block-start: 0.75rem; + } + } + + [tuiIconButton][data-appearance='icon'][data-size='xs'] { + float: inline-end; + margin-inline-end: -0.25rem; + } + + img { + display: block; + border-radius: var(--tui-radius-m); + } + + footer { + display: flex; + justify-content: flex-end; + gap: 0.5rem; + inline-size: 18rem; + margin: 1rem 0 0.25rem; + } + } +} diff --git a/projects/experimental/components/hint/index.ts b/projects/experimental/components/hint/index.ts new file mode 100644 index 000000000000..ce0f5a011255 --- /dev/null +++ b/projects/experimental/components/hint/index.ts @@ -0,0 +1 @@ +export * from './hint.component'; diff --git a/projects/experimental/components/hint/ng-package.json b/projects/experimental/components/hint/ng-package.json new file mode 100644 index 000000000000..bebf62dcb5e5 --- /dev/null +++ b/projects/experimental/components/hint/ng-package.json @@ -0,0 +1,5 @@ +{ + "lib": { + "entryFile": "index.ts" + } +} diff --git a/projects/experimental/components/index.ts b/projects/experimental/components/index.ts index 46e223f30f9f..85cf6ff4fb1b 100644 --- a/projects/experimental/components/index.ts +++ b/projects/experimental/components/index.ts @@ -1 +1,2 @@ +export * from '@taiga-ui/experimental/components/hint'; export * from '@taiga-ui/experimental/components/input-phone-international'; diff --git a/projects/experimental/components/input-phone-international/input-phone-international.component.ts b/projects/experimental/components/input-phone-international/input-phone-international.component.ts index 5cda76322dce..961eb2e7d53f 100644 --- a/projects/experimental/components/input-phone-international/input-phone-international.component.ts +++ b/projects/experimental/components/input-phone-international/input-phone-international.component.ts @@ -100,7 +100,7 @@ const NOT_FORM_CONTROL_SYMBOLS = /[^+\d]/g; '[disabled]': 'disabled()', '[value]': 'masked()', '(blur)': 'onTouched()', - '(input)': 'onInput()', + '(input)': 'onChange(unmasked)', '(click)': 'open.set(false)', '(beforeinput.capture)': 'onPaste($event)', }, @@ -184,12 +184,10 @@ export class TuiInputPhoneInternational extends TuiControl { this.dropdown.set(template); } - protected onInput(): void { + protected get unmasked(): string { const value = this.el.value.replaceAll(NOT_FORM_CONTROL_SYMBOLS, ''); - this.onChange( - value === tuiGetCallingCode(this.code(), this.metadata()) ? '' : value, - ); + return value === tuiGetCallingCode(this.code(), this.metadata()) ? '' : value; } protected onPaste(event: Event): void { @@ -212,12 +210,17 @@ export class TuiInputPhoneInternational extends TuiControl { } } - protected onItemClick(isoCode: TuiCountryIsoCode): void { + protected onItemClick(code: TuiCountryIsoCode): void { this.el.focus(); - this.el.value = this.el.value || tuiGetCallingCode(this.code(), this.metadata()); + this.el.value = this.unmasked; this.open.set(false); - this.code.set(isoCode); + this.code.set(code); this.search.set(''); + this.el.value = maskitoTransform( + this.el.value || tuiGetCallingCode(code, this.metadata()), + this.mask() || MASKITO_DEFAULT_OPTIONS, + ); + this.onChange(this.unmasked); } private computeMask( diff --git a/projects/experimental/components/input-phone-international/test/input-phone-international.component.spec.ts b/projects/experimental/components/input-phone-international/test/input-phone-international.component.spec.ts index 988b9d823da7..fa34d2581788 100644 --- a/projects/experimental/components/input-phone-international/test/input-phone-international.component.spec.ts +++ b/projects/experimental/components/input-phone-international/test/input-phone-international.component.spec.ts @@ -1,11 +1,11 @@ -import type {DebugElement} from '@angular/core'; import {ChangeDetectionStrategy, Component, ViewChild} from '@angular/core'; import type {ComponentFixture} from '@angular/core/testing'; import {TestBed} from '@angular/core/testing'; import {FormControl, ReactiveFormsModule} from '@angular/forms'; import {By} from '@angular/platform-browser'; -import {TuiRoot} from '@taiga-ui/core'; +import {TuiRoot, TuiTextfield} from '@taiga-ui/core'; import {NG_EVENT_PLUGINS} from '@taiga-ui/event-plugins'; +import {TuiInputPhoneInternational} from '@taiga-ui/experimental'; import type {TuiCountryIsoCode, TuiLanguage} from '@taiga-ui/i18n'; import { TUI_ENGLISH_LANGUAGE, @@ -13,10 +13,7 @@ import { TUI_LANGUAGE, TUI_RUSSIAN_LANGUAGE, } from '@taiga-ui/i18n'; -import { - TuiInputPhoneInternational, - tuiInputPhoneInternationalOptionsProvider, -} from '@taiga-ui/kit'; +import {tuiInputPhoneInternationalOptionsProvider} from '@taiga-ui/kit'; import {TuiNativeInputPO} from '@taiga-ui/testing'; import metadata from 'libphonenumber-js/max/metadata'; import {of} from 'rxjs'; @@ -24,15 +21,18 @@ import {of} from 'rxjs'; describe('InputPhoneInternational', () => { @Component({ standalone: true, - imports: [ReactiveFormsModule, TuiInputPhoneInternational, TuiRoot], + imports: [ReactiveFormsModule, TuiInputPhoneInternational, TuiRoot, TuiTextfield], template: ` - + + + `, changeDetection: ChangeDetectionStrategy.OnPush, @@ -90,7 +90,7 @@ describe('InputPhoneInternational', () => { initializeTestModule(); it('should switch country calling code and keeps all rest digits', async () => { - component.onItemClick('UA'); + component['onItemClick']('UA'); fixture.detectChanges(); await fixture.whenStable(); @@ -108,7 +108,7 @@ describe('InputPhoneInternational', () => { data, }); - component.onPaste(event); + component['onPaste'](event); fixture.detectChanges(); inputPO.sendText(data); @@ -188,7 +188,7 @@ describe('InputPhoneInternational', () => { initializeTestModule(TUI_RUSSIAN_LANGUAGE); it('displays country names in Russian inside dropdown', () => { - getCountrySelector().nativeElement.click(); + clickCountrySelector(); fixture.detectChanges(); expect(getDropdownCountryNames()).toEqual([ @@ -206,7 +206,7 @@ describe('InputPhoneInternational', () => { initializeTestModule(TUI_ENGLISH_LANGUAGE); it('displays country names in English inside dropdown', () => { - getCountrySelector().nativeElement.click(); + clickCountrySelector(); fixture.detectChanges(); expect(getDropdownCountryNames()).toEqual([ @@ -237,14 +237,16 @@ describe('InputPhoneInternational', () => { function getDropdownCountryNames(): string[] { const countryNameContainers = - fixture.debugElement.queryAll(By.css('.t-name')) || []; + fixture.debugElement.queryAll(By.css('[tuiTitle]')) || []; return countryNameContainers.map((container) => container.nativeElement.textContent?.trim(), ); } - function getCountrySelector(): DebugElement { - return fixture.debugElement.query(By.css('.t-select select')); + function clickCountrySelector(): void { + return fixture.debugElement + .query(By.css('.t-ipi-select')) + .nativeElement.dispatchEvent(new Event('mousedown')); } }); diff --git a/projects/kit/components/avatar/avatar.template.html b/projects/kit/components/avatar/avatar.template.html index e84d7d420682..22d1a278fadd 100644 --- a/projects/kit/components/avatar/avatar.template.html +++ b/projects/kit/components/avatar/avatar.template.html @@ -10,7 +10,5 @@ [icon]="value.toString()" /> {{ value }} - - - + diff --git a/projects/kit/components/pulse/pulse.component.ts b/projects/kit/components/pulse/pulse.component.ts index 0a53a071a842..88f66decb2f6 100644 --- a/projects/kit/components/pulse/pulse.component.ts +++ b/projects/kit/components/pulse/pulse.component.ts @@ -1,5 +1,14 @@ -import {ChangeDetectionStrategy, Component, inject, Input} from '@angular/core'; +import {isPlatformBrowser} from '@angular/common'; +import { + ChangeDetectionStrategy, + Component, + inject, + Input, + PLATFORM_ID, +} from '@angular/core'; +import {tuiInjectElement} from '@taiga-ui/cdk/utils/dom'; import {tuiFadeIn, tuiScaleIn} from '@taiga-ui/core/animations'; +import {tuiAsRectAccessor, TuiRectAccessor} from '@taiga-ui/core/classes'; import {TUI_ANIMATIONS_SPEED} from '@taiga-ui/core/tokens'; import {tuiToAnimationOptions} from '@taiga-ui/core/utils/miscellaneous'; @@ -9,6 +18,7 @@ import {tuiToAnimationOptions} from '@taiga-ui/core/utils/miscellaneous'; template: '', styleUrls: ['./pulse.style.less'], changeDetection: ChangeDetectionStrategy.OnPush, + providers: [tuiAsRectAccessor(TuiPulse)], animations: [tuiFadeIn, tuiScaleIn], host: { '[@tuiFadeIn]': 'animation', @@ -16,9 +26,22 @@ import {tuiToAnimationOptions} from '@taiga-ui/core/utils/miscellaneous'; '[style.--t-animation-state]': "playing ? 'running' : 'paused'", }, }) -export class TuiPulse { +export class TuiPulse extends TuiRectAccessor { + private readonly isBrowser = isPlatformBrowser(inject(PLATFORM_ID)); + private readonly el = tuiInjectElement(); + protected readonly animation = tuiToAnimationOptions(inject(TUI_ANIMATIONS_SPEED)); @Input() public playing = true; + + public readonly type = 'hint'; + + public getClientRect(): DOMRect { + const rect = this.el.getBoundingClientRect(); + + return this.isBrowser + ? new DOMRect(rect.x - 4, rect.y - 4, rect.width + 8, rect.height + 8) + : rect; + } } diff --git a/projects/kit/components/pulse/pulse.style.less b/projects/kit/components/pulse/pulse.style.less index d1ade1d66926..ed2a25fe94c4 100644 --- a/projects/kit/components/pulse/pulse.style.less +++ b/projects/kit/components/pulse/pulse.style.less @@ -1,47 +1,9 @@ -@keyframes tuiPulse { - 0% { - opacity: 0.3; - transform: scale(1); - } - - 20% { - opacity: 0; - transform: scale(0.5); - } - - 25% { - opacity: 0.3; - transform: scale(1); - } - - 45% { - opacity: 0; - transform: scale(0.5); - } - - 50% { - opacity: 0.3; - transform: scale(1); - } - - 70% { - opacity: 0; - transform: scale(0.5); - } - - 75% { - opacity: 0.3; - transform: scale(1); - } +@import '@taiga-ui/core/styles/taiga-ui-local'; - 95% { +@keyframes tuiPulse { + to { opacity: 0; - transform: scale(0.5); - } - - 100% { - opacity: 0.3; - transform: scale(1); + transform: scale(2.5); } } @@ -49,27 +11,25 @@ position: relative; color: var(--tui-background-accent-1); - &::before { + &::before, + &::after { content: ''; position: absolute; - top: -0.5rem; - left: -0.5rem; - inline-size: 1rem; - block-size: 1rem; + inline-size: 0.5rem; + block-size: 0.5rem; border-radius: 100%; + margin: -0.25rem; background: currentColor; - opacity: 0.3; - animation: tuiPulse 3s ease-in-out infinite; + } + + &::before { + opacity: 1; + transform: scale(0); + animation: tuiPulse 1s 1s ease-in-out infinite; animation-play-state: var(--t-animation-state); } &::after { - content: ''; - position: absolute; - inline-size: 0.5rem; - block-size: 0.5rem; - border-radius: 100%; - transform: translate(-50%, -50%); - background: currentColor; + box-shadow: 0 0 0.5rem; } }