From 791d2e5e23d63dbd65825e062d917905d483df18 Mon Sep 17 00:00:00 2001 From: desig9stein Date: Wed, 13 Aug 2025 16:07:18 +0300 Subject: [PATCH 1/3] fix(button): prevent style flickering on initial render by leveraging '--_ig-init-transition' --- .../components/button/_button-theme.scss | 8 +++---- .../icon-button/_icon-button-theme.scss | 21 +++++++++++++----- .../src/lib/directives/button/button-base.ts | 22 ++++++++++++++++--- .../button/icon-button.directive.ts | 8 ++++++- 4 files changed, 46 insertions(+), 13 deletions(-) diff --git a/projects/igniteui-angular/src/lib/core/styles/components/button/_button-theme.scss b/projects/igniteui-angular/src/lib/core/styles/components/button/_button-theme.scss index 26ba2b0a0f9..b438b2d5587 100644 --- a/projects/igniteui-angular/src/lib/core/styles/components/button/_button-theme.scss +++ b/projects/igniteui-angular/src/lib/core/styles/components/button/_button-theme.scss @@ -61,10 +61,10 @@ $variant ); - $button-transition: color $time ease-in-out, - background-color $time ease-in-out, - border-color $time ease-in-out, - box-shadow $time ease-in-out; + $button-transition: color var(--_ig-init-transition, #{$time}) ease-in-out, + background-color var(--_ig-init-transition, #{$time}) ease-in-out, + border-color var(--_ig-init-transition, #{$time}) ease-in-out, + box-shadow var(--_ig-init-transition, #{$time}) ease-in-out; $button-disabled-shadow: none; diff --git a/projects/igniteui-angular/src/lib/core/styles/components/icon-button/_icon-button-theme.scss b/projects/igniteui-angular/src/lib/core/styles/components/icon-button/_icon-button-theme.scss index 287af03d470..9327b7b8f9c 100644 --- a/projects/igniteui-angular/src/lib/core/styles/components/icon-button/_icon-button-theme.scss +++ b/projects/igniteui-angular/src/lib/core/styles/components/icon-button/_icon-button-theme.scss @@ -73,8 +73,10 @@ -webkit-tap-highlight-color: transparent; overflow: hidden; white-space: nowrap; - transition: box-shadow .2s ease-in, background .15s ease-out; - transition-delay: .05s; + transition: + box-shadow var(--_ig-init-transition, .2s) ease-in, + background var(--_ig-init-transition, .15s) ease-out; + transition-delay: var(--_ig-init-transition, .05s); min-width: unset; min-height: unset; font-size: rem(24px, 24px); @@ -95,7 +97,9 @@ } @if $variant == 'fluent' { - transition: color .15s ease-out, background .15s ease-out; + transition: + color var(--_ig-init-transition, .15s) ease-out, + background var(--_ig-init-transition, .15s) ease-out; &::after { position: absolute; @@ -109,11 +113,18 @@ } @if $variant == 'bootstrap' { - transition: box-shadow .15s ease-out, color .15s ease-out, background .15s ease-out; + transition: + box-shadow var(--_ig-init-transition, .15s) ease-out, + color var(--_ig-init-transition, .15s) ease-out, + background var(--_ig-init-transition, .15s) ease-out; } @if $variant == 'indigo' { - transition: color .15s ease-in-out, box-shadow .15s ease-in-out, background .15s ease-in-out, border-color .15s ease-in-out; + transition: + color var(--_ig-init-transition, .15s) ease-in-out, + box-shadow var(--_ig-init-transition, .15s) ease-in-out, + background var(--_ig-init-transition, .15s) ease-in-out, + border-color var(--_ig-init-transition, .15s) ease-in-out; } } diff --git a/projects/igniteui-angular/src/lib/directives/button/button-base.ts b/projects/igniteui-angular/src/lib/directives/button/button-base.ts index 3cad5e9d78d..e28e0ac1670 100644 --- a/projects/igniteui-angular/src/lib/directives/button/button-base.ts +++ b/projects/igniteui-angular/src/lib/directives/button/button-base.ts @@ -1,4 +1,6 @@ -import { Directive, ElementRef, EventEmitter, HostBinding, HostListener, Input, Output, booleanAttribute } from '@angular/core'; +import { + Directive, ElementRef, EventEmitter, HostBinding, HostListener, Input, Output, booleanAttribute, AfterViewInit, +} from '@angular/core'; export const IgxBaseButtonType = { Flat: 'flat', @@ -7,7 +9,7 @@ export const IgxBaseButtonType = { } as const; @Directive() -export abstract class IgxButtonBaseDirective { +export abstract class IgxButtonBaseDirective implements AfterViewInit { /** * Emitted when the button is clicked. */ @@ -79,7 +81,14 @@ export abstract class IgxButtonBaseDirective { return this.disabled || null; } - constructor(public element: ElementRef) { } + protected constructor( + public element: ElementRef, + ) { + // In browser, set via native API for immediate effect (no-op on server). + // In SSR there is no paint, so there’s no visual rendering or transitions to suppress. + // Fix style flickering https://github.com/IgniteUI/igniteui-angular/issues/14759 + this.element.nativeElement.style.setProperty('--_ig-init-transition', '0s'); + } /** * @hidden @@ -98,4 +107,11 @@ export abstract class IgxButtonBaseDirective { public get nativeElement() { return this.element.nativeElement; } + + public ngAfterViewInit() { + // Remove after the first frame to re-enable transitions + requestAnimationFrame(() => { + this.element.nativeElement.style.removeProperty('--_ig-init-transition'); + }); + } } diff --git a/projects/igniteui-angular/src/lib/directives/button/icon-button.directive.ts b/projects/igniteui-angular/src/lib/directives/button/icon-button.directive.ts index 25ef679aeba..6b3a2ba2db1 100644 --- a/projects/igniteui-angular/src/lib/directives/button/icon-button.directive.ts +++ b/projects/igniteui-angular/src/lib/directives/button/icon-button.directive.ts @@ -1,4 +1,4 @@ -import { Directive, HostBinding, Input } from '@angular/core'; +import {Directive, ElementRef, HostBinding, Input} from '@angular/core'; import { IgxBaseButtonType, IgxButtonBaseDirective } from './button-base'; /** @@ -78,4 +78,10 @@ export class IgxIconButtonDirective extends IgxButtonBaseDirective { public get outlined(): boolean { return this._type === IgxBaseButtonType.Outlined; } + + constructor( + public override element: ElementRef, + ) { + super(element); + } } From 2c59f521b0c0a7581572904bae53bbfe8dc81b3a Mon Sep 17 00:00:00 2001 From: Simeon Simeonoff Date: Mon, 15 Sep 2025 16:01:46 +0300 Subject: [PATCH 2/3] refactor(button): ensure the button works in SSR --- .../components/button/_button-theme.scss | 8 ++-- .../icon-button/_icon-button-theme.scss | 24 ++++++------ .../src/lib/directives/button/button-base.ts | 38 ++++++++++++++----- 3 files changed, 44 insertions(+), 26 deletions(-) diff --git a/projects/igniteui-angular/src/lib/core/styles/components/button/_button-theme.scss b/projects/igniteui-angular/src/lib/core/styles/components/button/_button-theme.scss index b438b2d5587..2b978400de7 100644 --- a/projects/igniteui-angular/src/lib/core/styles/components/button/_button-theme.scss +++ b/projects/igniteui-angular/src/lib/core/styles/components/button/_button-theme.scss @@ -61,10 +61,10 @@ $variant ); - $button-transition: color var(--_ig-init-transition, #{$time}) ease-in-out, - background-color var(--_ig-init-transition, #{$time}) ease-in-out, - border-color var(--_ig-init-transition, #{$time}) ease-in-out, - box-shadow var(--_ig-init-transition, #{$time}) ease-in-out; + $button-transition: color var(--_init-transition, #{$time}) ease-in-out, + background-color var(--_init-transition, #{$time}) ease-in-out, + border-color var(--_init-transition, #{$time}) ease-in-out, + box-shadow var(--_init-transition, #{$time}) ease-in-out; $button-disabled-shadow: none; diff --git a/projects/igniteui-angular/src/lib/core/styles/components/icon-button/_icon-button-theme.scss b/projects/igniteui-angular/src/lib/core/styles/components/icon-button/_icon-button-theme.scss index 9327b7b8f9c..9d5216b6d40 100644 --- a/projects/igniteui-angular/src/lib/core/styles/components/icon-button/_icon-button-theme.scss +++ b/projects/igniteui-angular/src/lib/core/styles/components/icon-button/_icon-button-theme.scss @@ -74,9 +74,9 @@ overflow: hidden; white-space: nowrap; transition: - box-shadow var(--_ig-init-transition, .2s) ease-in, - background var(--_ig-init-transition, .15s) ease-out; - transition-delay: var(--_ig-init-transition, .05s); + box-shadow var(--_init-transition, .2s) ease-in, + background var(--_init-transition, .15s) ease-out; + transition-delay: var(--_init-transition, .05s); min-width: unset; min-height: unset; font-size: rem(24px, 24px); @@ -98,8 +98,8 @@ @if $variant == 'fluent' { transition: - color var(--_ig-init-transition, .15s) ease-out, - background var(--_ig-init-transition, .15s) ease-out; + color var(--_init-transition, .15s) ease-out, + background var(--_init-transition, .15s) ease-out; &::after { position: absolute; @@ -114,17 +114,17 @@ @if $variant == 'bootstrap' { transition: - box-shadow var(--_ig-init-transition, .15s) ease-out, - color var(--_ig-init-transition, .15s) ease-out, - background var(--_ig-init-transition, .15s) ease-out; + box-shadow var(--_init-transition, .15s) ease-out, + color var(--_init-transition, .15s) ease-out, + background var(--_init-transition, .15s) ease-out; } @if $variant == 'indigo' { transition: - color var(--_ig-init-transition, .15s) ease-in-out, - box-shadow var(--_ig-init-transition, .15s) ease-in-out, - background var(--_ig-init-transition, .15s) ease-in-out, - border-color var(--_ig-init-transition, .15s) ease-in-out; + color var(--_init-transition, .15s) ease-in-out, + box-shadow var(--_init-transition, .15s) ease-in-out, + background var(--_init-transition, .15s) ease-in-out, + border-color var(--_init-transition, .15s) ease-in-out; } } diff --git a/projects/igniteui-angular/src/lib/directives/button/button-base.ts b/projects/igniteui-angular/src/lib/directives/button/button-base.ts index e28e0ac1670..ff04e2b93b1 100644 --- a/projects/igniteui-angular/src/lib/directives/button/button-base.ts +++ b/projects/igniteui-angular/src/lib/directives/button/button-base.ts @@ -1,6 +1,16 @@ import { - Directive, ElementRef, EventEmitter, HostBinding, HostListener, Input, Output, booleanAttribute, AfterViewInit, + Directive, + ElementRef, + EventEmitter, + HostBinding, + HostListener, + Input, + Output, + booleanAttribute, + inject, + afterRenderEffect, } from '@angular/core'; +import { PlatformUtil } from '../../core/utils'; export const IgxBaseButtonType = { Flat: 'flat', @@ -8,8 +18,12 @@ export const IgxBaseButtonType = { Outlined: 'outlined' } as const; + @Directive() -export abstract class IgxButtonBaseDirective implements AfterViewInit { +export abstract class IgxButtonBaseDirective { + private _platformUtil = inject(PlatformUtil); + private _elementRef = inject(ElementRef); + /** * Emitted when the button is clicked. */ @@ -87,7 +101,18 @@ export abstract class IgxButtonBaseDirective implements AfterViewInit { // In browser, set via native API for immediate effect (no-op on server). // In SSR there is no paint, so there’s no visual rendering or transitions to suppress. // Fix style flickering https://github.com/IgniteUI/igniteui-angular/issues/14759 - this.element.nativeElement.style.setProperty('--_ig-init-transition', '0s'); + if (this._platformUtil.isBrowser) { + afterRenderEffect({ + write: () => { + this.element.nativeElement.style.setProperty('--_init-transition', '0s'); + }, + read: () => { + requestAnimationFrame(() => { + this.element.nativeElement.style.removeProperty('--_init-transition'); + }); + } + }); + } } /** @@ -107,11 +132,4 @@ export abstract class IgxButtonBaseDirective implements AfterViewInit { public get nativeElement() { return this.element.nativeElement; } - - public ngAfterViewInit() { - // Remove after the first frame to re-enable transitions - requestAnimationFrame(() => { - this.element.nativeElement.style.removeProperty('--_ig-init-transition'); - }); - } } From c99c2ab658d95672e7f68b871673522c08eca9b3 Mon Sep 17 00:00:00 2001 From: Simeon Simeonoff Date: Mon, 15 Sep 2025 16:03:45 +0300 Subject: [PATCH 3/3] refactor(button): remove unused declaration --- .../igniteui-angular/src/lib/directives/button/button-base.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/projects/igniteui-angular/src/lib/directives/button/button-base.ts b/projects/igniteui-angular/src/lib/directives/button/button-base.ts index ff04e2b93b1..baeef654d91 100644 --- a/projects/igniteui-angular/src/lib/directives/button/button-base.ts +++ b/projects/igniteui-angular/src/lib/directives/button/button-base.ts @@ -22,7 +22,6 @@ export const IgxBaseButtonType = { @Directive() export abstract class IgxButtonBaseDirective { private _platformUtil = inject(PlatformUtil); - private _elementRef = inject(ElementRef); /** * Emitted when the button is clicked.