Skip to content
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ describe('IgxButtonGroup', () => {

expect(btnGroupInstance.selected.emit).not.toHaveBeenCalled();

btnGroupInstance.buttons[1].select();
btnGroupInstance.buttons[1].selected = true;
fixture.detectChanges();

expect(btnGroupInstance.selected.emit).not.toHaveBeenCalled();
Expand All @@ -126,16 +126,16 @@ describe('IgxButtonGroup', () => {
fixture.detectChanges();

const btnGroupInstance = fixture.componentInstance.buttonGroup;
btnGroupInstance.buttons[0].select();
btnGroupInstance.buttons[1].select();
btnGroupInstance.buttons[0].selected = true;
btnGroupInstance.buttons[1].selected = true;
spyOn(btnGroupInstance.deselected, 'emit');

btnGroupInstance.ngAfterViewInit();
fixture.detectChanges();

expect(btnGroupInstance.deselected.emit).not.toHaveBeenCalled();

btnGroupInstance.buttons[1].deselect();
btnGroupInstance.buttons[1].selected = false;
fixture.detectChanges();

expect(btnGroupInstance.deselected.emit).not.toHaveBeenCalled();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -330,8 +330,8 @@ export class IgxButtonGroupComponent implements AfterViewInit, OnDestroy {
return;
}

const button = this.buttons[index];
button.select();
this.buttons[index].selected = true;
this.updateSelected(index);
}

/**
Expand Down Expand Up @@ -398,8 +398,8 @@ export class IgxButtonGroupComponent implements AfterViewInit, OnDestroy {
return;
}

const button = this.buttons[index];
button.deselect();
this.buttons[index].selected = false;
this.updateDeselected(index);
}

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -1,18 +1,6 @@
import {
Directive,
ElementRef,
EventEmitter,
HostBinding,
HostListener,
Input,
Output,
booleanAttribute,
inject,
AfterViewInit,
OnDestroy
} from '@angular/core';
import { PlatformUtil } from 'igniteui-angular/core';
import { animationFrameScheduler, Subscription } from 'rxjs';
import { Directive, ElementRef, Input, booleanAttribute, inject, AfterViewInit, signal, EventEmitter, Output} from '@angular/core';
import { IgxFocusRingDirective } from '../focus-ring/focus-ring.directive';


export const IgxBaseButtonType = {
Flat: 'flat',
Expand All @@ -21,126 +9,53 @@ export const IgxBaseButtonType = {
} as const;


@Directive()
export abstract class IgxButtonBaseDirective implements AfterViewInit, OnDestroy {
private _platformUtil = inject(PlatformUtil);
public element = inject(ElementRef);
private _viewInit = false;
private _animationScheduler: Subscription;
@Directive({
host: {
'role': 'button',
'[attr.disabled]': '_disabled() || null',
'[class.igx-button--focused]': '_hasKeyboardFocus()',
'[class.igx-button--disabled]': '_disabled()',
'[style.--_init-transition]': '_hasRendered() ? null : "0s"',
'[style.transition]': '_hasRendered() ? "var(--_button-transition)" : "none"',
'(click)': 'buttonClick.emit($event)',
},
hostDirectives: [IgxFocusRingDirective]
})
export abstract class IgxButtonBaseDirective implements AfterViewInit {
protected readonly _element = inject<ElementRef<HTMLElement>>(ElementRef);
protected readonly _hasKeyboardFocus = inject(IgxFocusRingDirective).hasKeyboardFocus;

/**
* Emitted when the button is clicked.
*/
@Output()
public buttonClick = new EventEmitter<any>();
protected readonly _hasRendered = signal(false);
protected readonly _disabled = signal(false);

/**
* Sets/gets the `role` attribute.
* Gets or sets whether the button is disabled.
*
* @example
* ```typescript
* this.button.role = 'navbutton';
* let buttonRole = this.button.role;
* ```
*/
@HostBinding('attr.role')
public role = 'button';

/**
* @hidden
* @internal
*/
@HostListener('click', ['$event'])
public onClick(ev: MouseEvent) {
this.buttonClick.emit(ev);
this.focused = false;
}

/**
* @hidden
* @internal
*/
@HostListener('blur')
protected onBlur() {
this.focused = false;
}

/**
* Sets/gets whether the button component is on focus.
* Default value is `false`.
* ```typescript
* this.button.focus = true;
* ```
* ```typescript
* let isFocused = this.button.focused;
* ```html
* <button type="button" igxButton="flat" [disabled]="isDisabled"></button>
* ```
*/
@HostBinding('class.igx-button--focused')
protected focused = false;

/**
* Enables/disables the button.
*
* @example
* ```html
* <button igxButton="fab" disabled></button>
* ```
*/
@Input({ transform: booleanAttribute })
@HostBinding('class.igx-button--disabled')
public disabled = false;

/**
* @hidden
* @internal
*/
@HostBinding('attr.disabled')
public get disabledAttribute() {
return this.disabled || null;
}

protected constructor() {
// 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
if (this._platformUtil.isBrowser) {
this.element.nativeElement.style.setProperty('--_init-transition', '0s');
this.element.nativeElement.style.setProperty('transition', 'none');
}
public set disabled(value: boolean) {
this._disabled.set(value);
}

public ngAfterViewInit(): void {
if (this._platformUtil.isBrowser && !this._viewInit) {
this._viewInit = true;

this._animationScheduler = animationFrameScheduler.schedule(() => {
this.element.nativeElement.style.removeProperty('--_init-transition');
this.element.nativeElement.style.setProperty('transition', 'var(--_button-transition)');
});
}
public get disabled(): boolean {
return this._disabled();
}

public ngOnDestroy(): void {
if (this._animationScheduler) {
this._animationScheduler.unsubscribe();
}
}
/** Emitted when the button is clicked. */
@Output()
public readonly buttonClick = new EventEmitter<MouseEvent>();

/**
* @hidden
* @internal
*/
@HostListener('keyup', ['$event'])
protected updateOnKeyUp(event: KeyboardEvent) {
if (event.key === "Tab") {
this.focused = true;
}
/** Returns the underlying DOM element. */
public get nativeElement(): HTMLButtonElement {
return this._element.nativeElement as HTMLButtonElement;
Comment thread
rkaraivanov marked this conversation as resolved.
Outdated
}

/**
* Returns the underlying DOM element.
*/
public get nativeElement() {
return this.element.nativeElement;
public ngAfterViewInit(): void {
// FUOC workaround - ensures that the transition is only applied after the initial render
Copy link

Copilot AI Apr 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The FOUC/transition suppression logic now flips _hasRendered to true synchronously in ngAfterViewInit(). Previously this was deferred to the next animation frame in the browser to ensure transitions are only enabled after the first paint (see the removed animationFrameScheduler.schedule(...) logic). This change can reintroduce the flicker the workaround was meant to address; consider restoring the “next frame” deferral (browser-only) rather than enabling transitions immediately in ngAfterViewInit().

Suggested change
// FUOC workaround - ensures that the transition is only applied after the initial render
// FUOC workaround - ensures that the transition is only applied after the initial render
if (typeof requestAnimationFrame === 'function') {
requestAnimationFrame(() => this._hasRendered.set(true));
return;
}

Copilot uses AI. Check for mistakes.
this._hasRendered.set(true);
}
}
Loading
Loading