From b7dd1ff6d8b9dd811ced58b430fe11f28f84ccfe Mon Sep 17 00:00:00 2001 From: Artem Belik Date: Thu, 13 Aug 2026 15:56:01 +0300 Subject: [PATCH 1/2] feat(code-block): added alwaysShowActionbar attribute (#DS-4721) --- .../components/code-block/code-block.en.md | 10 ++- .../components/code-block/code-block.ru.md | 10 ++- .../components/code-block/code-block.spec.ts | 36 +++++++++ packages/components/code-block/code-block.ts | 80 +++++++------------ .../code-block-with-filled-example.ts | 11 ++- .../components/code-block.api.md | 4 +- 6 files changed, 91 insertions(+), 60 deletions(-) diff --git a/packages/components/code-block/code-block.en.md b/packages/components/code-block/code-block.en.md index a367a086e6..5ba9a3730e 100644 --- a/packages/components/code-block/code-block.en.md +++ b/packages/components/code-block/code-block.en.md @@ -86,7 +86,15 @@ When the code block should fill an entire container or screen, it is best to use ### Action panel -The action panel is located in the upper right corner of the block, visible on hover or when one of the buttons is focused, and stays fixed during scrolling. The component configuration determines which actions are available. +The action panel is located in the upper-right corner of the block and remains fixed while scrolling. The component configuration determines which actions are available. + +The panel is always visible when tabs are shown, as well as on iOS and Android devices. When tabs are hidden, it appears on hover on other devices. + +The `alwaysShowActionbar` attribute keeps the panel visible regardless of tabs or hover. It is disabled by default. + +```html + +``` #### Changing wrap mode diff --git a/packages/components/code-block/code-block.ru.md b/packages/components/code-block/code-block.ru.md index c5ff2f907e..45179820d2 100644 --- a/packages/components/code-block/code-block.ru.md +++ b/packages/components/code-block/code-block.ru.md @@ -86,7 +86,15 @@ providers: [ ### Панель действий -Панель действий расположена в правом верхнем углу блока, она видна по ховеру на блок или при фокусе на одну из кнопок, залипает при прокрутке. Конфигурация компонента определяет, какие действия будут доступны. +Панель действий расположена в правом верхнем углу блока и остается на месте при прокрутке. Конфигурация компонента определяет, какие действия будут доступны. + +При видимых вкладках, а также на устройствах iOS и Android панель отображается постоянно. Если вкладки скрыты, на остальных устройствах панель появляется при наведении на блок. + +Атрибут `alwaysShowActionbar` позволяет отображать панель постоянно независимо от наличия вкладок и наведения. По умолчанию он выключен. + +```html + +``` #### Изменение режима переноса diff --git a/packages/components/code-block/code-block.spec.ts b/packages/components/code-block/code-block.spec.ts index 9a5007c884..3667b67e47 100644 --- a/packages/components/code-block/code-block.spec.ts +++ b/packages/components/code-block/code-block.spec.ts @@ -92,6 +92,7 @@ const mockPreHeight = (debugElement: DebugElement, height: number): void => { [canDownload]="canDownload" [noBorder]="noBorder" [hideTabs]="hideTabs" + [alwaysShowActionbar]="alwaysShowActionbar" [canCopy]="canCopy" [maxHeight]="maxHeight" [(activeFileIndex)]="activeFileIndex" @@ -126,6 +127,7 @@ class BaseCodeBlock { activeFileIndex: number = 0; noBorder: boolean = false; hideTabs: boolean = false; + alwaysShowActionbar: boolean = false; softWrap: boolean = false; maxHeight: number | undefined = undefined; } @@ -515,6 +517,40 @@ describe(KbqCodeBlock.name, () => { expect(codeBlock.classes['kbq-code-block_show-actionbar']).toBeFalsy(); })); + it('should always show actionbar when alwaysShowActionbar is enabled', fakeAsync(() => { + const fixture = createComponent(BaseCodeBlock); + const { debugElement, componentInstance } = fixture; + const codeBlock = geCodeBlockDebugElement(debugElement); + + componentInstance.hideTabs = true; + componentInstance.alwaysShowActionbar = true; + fixture.detectChanges(); + expect(codeBlock.classes['kbq-code-block_show-actionbar']).toBeTruthy(); + + codeBlock.nativeElement.dispatchEvent(new MouseEvent('mouseleave')); + tick(HOVER_DEBOUNCE_TIME); + expect(codeBlock.classes['kbq-code-block_show-actionbar']).toBeTruthy(); + })); + + it('should restore hover behavior when alwaysShowActionbar is disabled', fakeAsync(() => { + const fixture = createComponent(BaseCodeBlock); + const { debugElement, componentInstance } = fixture; + const codeBlock = geCodeBlockDebugElement(debugElement); + + componentInstance.hideTabs = true; + componentInstance.alwaysShowActionbar = true; + fixture.detectChanges(); + expect(codeBlock.classes['kbq-code-block_show-actionbar']).toBeTruthy(); + + componentInstance.alwaysShowActionbar = false; + fixture.detectChanges(); + expect(codeBlock.classes['kbq-code-block_show-actionbar']).toBeFalsy(); + + codeBlock.nativeElement.dispatchEvent(new MouseEvent('mouseenter')); + tick(HOVER_DEBOUNCE_TIME); + expect(codeBlock.classes['kbq-code-block_show-actionbar']).toBeTruthy(); + })); + it('should stop tracking hover when hideTabs changes to false', fakeAsync(() => { const fixture = createComponent(BaseCodeBlock); const { debugElement, componentInstance } = fixture; diff --git a/packages/components/code-block/code-block.ts b/packages/components/code-block/code-block.ts index 44f91736e6..8ca1434c3f 100644 --- a/packages/components/code-block/code-block.ts +++ b/packages/components/code-block/code-block.ts @@ -10,6 +10,7 @@ import { ChangeDetectionStrategy, ChangeDetectorRef, Component, + computed, ContentChild, DestroyRef, Directive, @@ -22,7 +23,6 @@ import { numberAttribute, output, Provider, - Renderer2, SecurityContext, signal, TemplateRef, @@ -44,7 +44,7 @@ import { import { KbqIconModule } from '@koobiq/components/icon'; import { KbqTabsModule } from '@koobiq/components/tabs'; import { KbqToolTipModule, KbqTooltipTrigger } from '@koobiq/components/tooltip'; -import { debounceTime, EMPTY, filter, fromEvent, merge, startWith, switchMap, take } from 'rxjs'; +import { debounceTime, filter, fromEvent, map, merge, of, startWith, switchMap, take } from 'rxjs'; import { KbqCodeBlockHighlight } from './code-block-highlight'; import { KbqCodeBlockFile, KbqTabLinkTemplateContext } from './types'; @@ -109,6 +109,7 @@ export class KbqCodeBlockTabLinkContent {} '[class.kbq-code-block_hide-line-numbers]': '!lineNumbers()', '[class.kbq-code-block_hide-tabs]': 'hideTabs', '[class.kbq-code-block_no-border]': 'noBorder() || filled()', + '[class.kbq-code-block_show-actionbar]': 'actionbarVisible()', '[class.kbq-code-block_soft-wrap]': 'softWrap', '[class.kbq-code-block_view-all]': 'viewAll' }, @@ -203,6 +204,9 @@ export class KbqCodeBlock implements AfterViewInit { /** Added copy code button. */ readonly canCopy = input(true, { transform: booleanAttribute }); + /** Whether the actionbar should remain visible when tabs are hidden. */ + readonly alwaysShowActionbar = input(false, { transform: booleanAttribute }); + /** * @deprecated Will be removed in next major release, use `files` instead. */ @@ -261,16 +265,16 @@ export class KbqCodeBlock implements AfterViewInit { // Accessor inputs cannot be migrated as they are too complex. @Input({ transform: booleanAttribute }) get hideTabs(): boolean { - return this._hideTabs; + return this._hideTabs(); } set hideTabs(value: boolean) { - this._hideTabs = value; + this._hideTabs.set(value); this.hideTabsChange.emit(value); - this.setupActionbarDisplay(); } - private _hideTabs: boolean = false; + private readonly _hideTabs = signal(false); + private readonly actionbarHovered = signal(false); /** * Output to support two-way binding on `[(hideTabs)]` property. @@ -325,7 +329,6 @@ export class KbqCodeBlock implements AfterViewInit { private readonly changeDetectorRef = inject(ChangeDetectorRef); private readonly localeService = inject(KBQ_LOCALE_SERVICE, { optional: true }); private readonly destroyRef = inject(DestroyRef); - private readonly renderer = inject(Renderer2); private readonly platform = inject(Platform); private readonly focusMonitor = inject(FocusMonitor); private readonly clipboard = inject(Clipboard); @@ -338,6 +341,16 @@ export class KbqCodeBlock implements AfterViewInit { protected readonly fallbackFileName = inject(KBQ_CODE_BLOCK_FALLBACK_FILE_NAME); private readonly window = inject(KBQ_WINDOW); + /** @docs-private */ + protected readonly actionbarVisible = computed( + () => + this.alwaysShowActionbar() || + this.platform.IOS || + this.platform.ANDROID || + !this._hideTabs() || + this.actionbarHovered() + ); + constructor() { this.localeService?.changes.pipe(takeUntilDestroyed()).subscribe(this.updateLocaleParams); } @@ -346,9 +359,6 @@ export class KbqCodeBlock implements AfterViewInit { this.trackHoverState(); this.setupContentOverflowDetection(); - // Setup initial actionbar display state - this.setupActionbarDisplay(); - this.copyButtonTooltip() ?.visibleChange.pipe(takeUntilDestroyed(this.destroyRef)) .subscribe((isVisible) => { @@ -434,19 +444,19 @@ export class KbqCodeBlock implements AfterViewInit { private trackHoverState(): void { outputToObservable(this.hideTabsChange) .pipe( - startWith(this._hideTabs), + startWith(this._hideTabs()), switchMap((hideTabs) => { - if (!hideTabs) return EMPTY; + if (!hideTabs) return of(false); return merge( - fromEvent(this.elementRef.nativeElement, 'mouseenter'), - fromEvent(this.elementRef.nativeElement, 'mouseleave') - ).pipe(debounceTime(100)); + fromEvent(this.elementRef.nativeElement, 'mouseenter').pipe(map(() => true)), + fromEvent(this.elementRef.nativeElement, 'mouseleave').pipe(map(() => false)) + ).pipe(debounceTime(100), startWith(false)); }), takeUntilDestroyed(this.destroyRef) ) - .subscribe((event) => { - this.setupActionbarDisplay(event?.type === 'mouseenter'); + .subscribe((isHovered) => { + this.actionbarHovered.set(isHovered); }); } @@ -478,42 +488,6 @@ export class KbqCodeBlock implements AfterViewInit { .subscribe(checkOverflow); } - /** - * Adds or removes the actionbar display class from the code block based on the specified condition. - * - * The actionbar is always visible on mobile devices and when the tabs are visible. - * Otherwise, the actionbar is only visible when the mouse is hovered over the code block. - * - * @param shouldShowActionbar - A boolean indicating whether the actionbar should be visible. - */ - private setupActionbarDisplay(shouldShowActionbar?: boolean): void { - const className = 'kbq-code-block_show-actionbar'; - - // Should always show actionbar on Mobile devices - if (this.platform.IOS || this.platform.ANDROID) { - this.renderer.addClass(this.elementRef.nativeElement, className); - - return; - } - - // Should always show actionbar when tabs are visible - if (!this.hideTabs) { - this.renderer.addClass(this.elementRef.nativeElement, className); - - return; - } - - if (typeof shouldShowActionbar === 'undefined') { - return; - } - - if (shouldShowActionbar) { - this.renderer.addClass(this.elementRef.nativeElement, className); - } else { - this.renderer.removeClass(this.elementRef.nativeElement, className); - } - } - /** Whether the element has scroll. */ private hasScroll({ scrollHeight, scrollWidth, clientHeight, clientWidth }: HTMLElement): boolean { return scrollHeight > clientHeight || scrollWidth > clientWidth; diff --git a/packages/docs-examples/components/code-block/code-block-with-filled/code-block-with-filled-example.ts b/packages/docs-examples/components/code-block/code-block-with-filled/code-block-with-filled-example.ts index 3cb749b5e8..096c814ada 100644 --- a/packages/docs-examples/components/code-block/code-block-with-filled/code-block-with-filled-example.ts +++ b/packages/docs-examples/components/code-block/code-block-with-filled/code-block-with-filled-example.ts @@ -1,4 +1,4 @@ -import { ChangeDetectionStrategy, Component } from '@angular/core'; +import { ChangeDetectionStrategy, Component, model } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { KbqCodeBlockFile, @@ -18,8 +18,10 @@ import { KbqToggleModule } from '@koobiq/components/toggle'; FormsModule ], template: ` - Filled - + Filled + Always show actionbar + + `, providers: [ kbqCodeBlockHighlightJsConfigProvider({ @@ -39,5 +41,6 @@ export class CodeBlockWithFilledExample { } ]; - filled: boolean = true; + readonly filled = model(true); + readonly alwaysShowActionbar = model(false); } diff --git a/tools/public_api_guard/components/code-block.api.md b/tools/public_api_guard/components/code-block.api.md index 130ed5a3f1..961c66f3ed 100644 --- a/tools/public_api_guard/components/code-block.api.md +++ b/tools/public_api_guard/components/code-block.api.md @@ -39,8 +39,10 @@ export const KBQ_CODE_BLOCK_LOCALE_CONFIGURATION: InjectionToken; activeFileIndex: number; readonly activeFileIndexChange: _angular_core.OutputEmitterRef; + readonly alwaysShowActionbar: _angular_core.InputSignalWithTransform; protected readonly buttonStyle: typeof KbqButtonStyles; protected get calculatedMaxHeight(): number | null; readonly canCopy: _angular_core.InputSignalWithTransform; @@ -94,7 +96,7 @@ export class KbqCodeBlock implements AfterViewInit { viewAll: boolean; readonly viewAllChange: _angular_core.OutputEmitterRef; // (undocumented) - static ɵcmp: _angular_core.ɵɵComponentDeclaration; + static ɵcmp: _angular_core.ɵɵComponentDeclaration; // (undocumented) static ɵfac: _angular_core.ɵɵFactoryDeclaration; } From 831792d1225732e231e9a028afa82227907c62b0 Mon Sep 17 00:00:00 2001 From: Artem Belik Date: Fri, 14 Aug 2026 11:37:17 +0300 Subject: [PATCH 2/2] refactor: review --- .../components/code-block/code-block.en.md | 6 ++ .../components/code-block/code-block.ru.md | 6 ++ .../components/code-block/code-block.spec.ts | 85 ++++++++++++++++++- packages/components/code-block/code-block.ts | 72 ++++++++++------ .../components/code-block.api.md | 13 +++ 5 files changed, 157 insertions(+), 25 deletions(-) diff --git a/packages/components/code-block/code-block.en.md b/packages/components/code-block/code-block.en.md index 5ba9a3730e..d9e12bbd4a 100644 --- a/packages/components/code-block/code-block.en.md +++ b/packages/components/code-block/code-block.en.md @@ -96,6 +96,12 @@ The `alwaysShowActionbar` attribute keeps the panel visible regardless of tabs o ``` +To enable the option for all code blocks within an injector scope, use the provider: + +```typescript +providers: [kbqCodeBlockDefaultOptionsProvider({ alwaysShowActionbar: true })]; +``` + #### Changing wrap mode The user can toggle the wrap mode using a toggle button; this option is disabled by default and can be configured using the `canToggleSoftWrap` attribute. diff --git a/packages/components/code-block/code-block.ru.md b/packages/components/code-block/code-block.ru.md index 45179820d2..07fe9631cb 100644 --- a/packages/components/code-block/code-block.ru.md +++ b/packages/components/code-block/code-block.ru.md @@ -96,6 +96,12 @@ providers: [ ``` +Чтобы включить настройку для всех блоков кода, зарегистрируйте провайдер: + +```typescript +providers: [kbqCodeBlockDefaultOptionsProvider({ alwaysShowActionbar: true })]; +``` + #### Изменение режима переноса Пользователь может изменять режим переноса при помощи кнопки-переключателя, по умолчанию эта возможность выключена, настраивается при помощи атрибута `canToggleSoftWrap`. diff --git a/packages/components/code-block/code-block.spec.ts b/packages/components/code-block/code-block.spec.ts index 3667b67e47..25a3da863e 100644 --- a/packages/components/code-block/code-block.spec.ts +++ b/packages/components/code-block/code-block.spec.ts @@ -7,7 +7,12 @@ import { NoopAnimationsModule } from '@angular/platform-browser/animations'; import { KbqTabNavBar } from '@koobiq/components/tabs'; import { HLJSApi } from 'highlight.js'; import { Observable, Subject } from 'rxjs'; -import { KBQ_CODE_BLOCK_FALLBACK_FILE_NAME, KbqCodeBlock, kbqCodeBlockLocaleConfigurationProvider } from './code-block'; +import { + KBQ_CODE_BLOCK_FALLBACK_FILE_NAME, + KbqCodeBlock, + kbqCodeBlockDefaultOptionsProvider, + kbqCodeBlockLocaleConfigurationProvider +} from './code-block'; import { KBQ_CODE_BLOCK_FALLBACK_FILE_LANGUAGE, KbqCodeBlockHighlight, @@ -132,6 +137,17 @@ class BaseCodeBlock { maxHeight: number | undefined = undefined; } +@Component({ + imports: [KbqCodeBlockModule], + template: ` + + `, + changeDetection: ChangeDetectionStrategy.OnPush +}) +class CodeBlockWithDefaultOptions { + readonly files: KbqCodeBlockFile[] = [{ language: 'typescript', filename: 'main.ts', content: 'const value = 1;' }]; +} + @Component({ imports: [KbqCodeBlockModule], template: ` @@ -532,6 +548,73 @@ describe(KbqCodeBlock.name, () => { expect(codeBlock.classes['kbq-code-block_show-actionbar']).toBeTruthy(); })); + it('should use alwaysShowActionbar from default options', () => { + const { debugElement } = createComponent(CodeBlockWithDefaultOptions, [ + kbqCodeBlockDefaultOptionsProvider({ alwaysShowActionbar: true }) + ]); + const codeBlock = geCodeBlockDebugElement(debugElement); + + expect(codeBlock.componentInstance.alwaysShowActionbar()).toBeTruthy(); + expect(codeBlock.classes['kbq-code-block_show-actionbar']).toBeTruthy(); + }); + + it('should override alwaysShowActionbar from default options with input', () => { + const fixture = createComponent(BaseCodeBlock, [ + kbqCodeBlockDefaultOptionsProvider({ alwaysShowActionbar: true }) + ]); + const { debugElement, componentInstance } = fixture; + const codeBlock = geCodeBlockDebugElement(debugElement); + + componentInstance.hideTabs = true; + fixture.detectChanges(); + + expect(codeBlock.componentInstance.alwaysShowActionbar()).toBeFalsy(); + expect(codeBlock.classes['kbq-code-block_show-actionbar']).toBeFalsy(); + }); + + it('should not track hover when alwaysShowActionbar is enabled', () => { + const addEventListenerSpy = jest.spyOn(HTMLElement.prototype, 'addEventListener'); + + try { + TestBed.configureTestingModule({ imports: [BaseCodeBlock, NoopAnimationsModule] }); + const fixture = TestBed.createComponent(BaseCodeBlock); + + fixture.componentInstance.hideTabs = true; + fixture.componentInstance.alwaysShowActionbar = true; + fixture.detectChanges(); + + const codeBlockElement = geCodeBlockDebugElement(fixture.debugElement).nativeElement; + const hostHoverListeners = addEventListenerSpy.mock.calls.filter( + ([eventName], index) => + addEventListenerSpy.mock.contexts[index] === codeBlockElement && + (eventName === 'mouseenter' || eventName === 'mouseleave') + ); + + expect(hostHoverListeners).toHaveLength(0); + } finally { + addEventListenerSpy.mockRestore(); + } + }); + + it('should start tracking hover when alwaysShowActionbar is disabled', fakeAsync(() => { + TestBed.configureTestingModule({ imports: [BaseCodeBlock, NoopAnimationsModule] }); + const fixture = TestBed.createComponent(BaseCodeBlock); + + fixture.componentInstance.hideTabs = true; + fixture.componentInstance.alwaysShowActionbar = true; + fixture.detectChanges(); + + fixture.componentInstance.alwaysShowActionbar = false; + fixture.detectChanges(); + + const codeBlock = geCodeBlockDebugElement(fixture.debugElement); + + codeBlock.nativeElement.dispatchEvent(new MouseEvent('mouseenter')); + tick(HOVER_DEBOUNCE_TIME); + fixture.detectChanges(); + expect(codeBlock.classes['kbq-code-block_show-actionbar']).toBeTruthy(); + })); + it('should restore hover behavior when alwaysShowActionbar is disabled', fakeAsync(() => { const fixture = createComponent(BaseCodeBlock); const { debugElement, componentInstance } = fixture; diff --git a/packages/components/code-block/code-block.ts b/packages/components/code-block/code-block.ts index 8ca1434c3f..4a08e217bf 100644 --- a/packages/components/code-block/code-block.ts +++ b/packages/components/code-block/code-block.ts @@ -14,6 +14,7 @@ import { ContentChild, DestroyRef, Directive, + effect, ElementRef, inject, InjectionToken, @@ -29,7 +30,7 @@ import { viewChild, ViewEncapsulation } from '@angular/core'; -import { outputToObservable, takeUntilDestroyed, toObservable } from '@angular/core/rxjs-interop'; +import { takeUntilDestroyed, toObservable } from '@angular/core/rxjs-interop'; import { DomSanitizer } from '@angular/platform-browser'; import { KbqButtonModule, KbqButtonStyles } from '@koobiq/components/button'; import { @@ -44,7 +45,7 @@ import { import { KbqIconModule } from '@koobiq/components/icon'; import { KbqTabsModule } from '@koobiq/components/tabs'; import { KbqToolTipModule, KbqTooltipTrigger } from '@koobiq/components/tooltip'; -import { debounceTime, filter, fromEvent, map, merge, of, startWith, switchMap, take } from 'rxjs'; +import { debounceTime, filter, fromEvent, map, merge, take } from 'rxjs'; import { KbqCodeBlockHighlight } from './code-block-highlight'; import { KbqCodeBlockFile, KbqTabLinkTemplateContext } from './types'; @@ -71,6 +72,23 @@ export const kbqCodeBlockFallbackFileNameProvider = (fileName: string): Provider useValue: fileName }); +/** Default options for `kbq-code-block`. */ +export type KbqCodeBlockDefaultOptions = Partial<{ + /** Whether the actionbar should remain visible when tabs are hidden. */ + alwaysShowActionbar: boolean; +}>; + +/** Injection token used to configure the default options for all `kbq-code-block` components. */ +export const KBQ_CODE_BLOCK_DEFAULT_OPTIONS = new InjectionToken( + 'KBQ_CODE_BLOCK_DEFAULT_OPTIONS' +); + +/** Utility provider for `KBQ_CODE_BLOCK_DEFAULT_OPTIONS`. */ +export const kbqCodeBlockDefaultOptionsProvider = (options: KbqCodeBlockDefaultOptions): Provider => ({ + provide: KBQ_CODE_BLOCK_DEFAULT_OPTIONS, + useValue: options +}); + /** Marks a template as a custom tab link. */ @Directive({ selector: 'ng-template[kbqCodeBlockTabLinkContent]', @@ -117,6 +135,7 @@ export class KbqCodeBlockTabLinkContent {} }) export class KbqCodeBlock implements AfterViewInit { private readonly copyButtonTooltip = viewChild('copyButtonTooltip'); + private readonly defaultOptions = inject(KBQ_CODE_BLOCK_DEFAULT_OPTIONS, { optional: true }); /** * Reference to the scrollable code content. * @@ -205,7 +224,9 @@ export class KbqCodeBlock implements AfterViewInit { readonly canCopy = input(true, { transform: booleanAttribute }); /** Whether the actionbar should remain visible when tabs are hidden. */ - readonly alwaysShowActionbar = input(false, { transform: booleanAttribute }); + readonly alwaysShowActionbar = input(this.defaultOptions?.alwaysShowActionbar ?? false, { + transform: booleanAttribute + }); /** * @deprecated Will be removed in next major release, use `files` instead. @@ -352,11 +373,11 @@ export class KbqCodeBlock implements AfterViewInit { ); constructor() { + this.trackHoverState(); this.localeService?.changes.pipe(takeUntilDestroyed()).subscribe(this.updateLocaleParams); } ngAfterViewInit(): void { - this.trackHoverState(); this.setupContentOverflowDetection(); this.copyButtonTooltip() @@ -437,27 +458,30 @@ export class KbqCodeBlock implements AfterViewInit { } } - /** - * Tracks hover events to show/hide the actionbar when `hideTabs` is `true`. - * Reacts to `hideTabs` changes dynamically. - */ + /** Tracks hover when tabs are hidden and `alwaysShowActionbar` is disabled. */ private trackHoverState(): void { - outputToObservable(this.hideTabsChange) - .pipe( - startWith(this._hideTabs()), - switchMap((hideTabs) => { - if (!hideTabs) return of(false); - - return merge( - fromEvent(this.elementRef.nativeElement, 'mouseenter').pipe(map(() => true)), - fromEvent(this.elementRef.nativeElement, 'mouseleave').pipe(map(() => false)) - ).pipe(debounceTime(100), startWith(false)); - }), - takeUntilDestroyed(this.destroyRef) - ) - .subscribe((isHovered) => { - this.actionbarHovered.set(isHovered); - }); + effect( + (onCleanup) => { + const hideTabs = this._hideTabs(); + const alwaysShowActionbar = this.alwaysShowActionbar(); + + this.actionbarHovered.set(false); + + if (!hideTabs || alwaysShowActionbar || this.platform.IOS || this.platform.ANDROID) return; + + const subscription = merge( + fromEvent(this.elementRef.nativeElement, 'mouseenter').pipe(map(() => true)), + fromEvent(this.elementRef.nativeElement, 'mouseleave').pipe(map(() => false)) + ) + .pipe(debounceTime(100)) + .subscribe((isHovered) => { + this.actionbarHovered.set(isHovered); + }); + + onCleanup(() => subscription.unsubscribe()); + }, + { injector: this.injector } + ); } private setupContentOverflowDetection(): void { diff --git a/tools/public_api_guard/components/code-block.api.md b/tools/public_api_guard/components/code-block.api.md index 961c66f3ed..05fb0eca39 100644 --- a/tools/public_api_guard/components/code-block.api.md +++ b/tools/public_api_guard/components/code-block.api.md @@ -17,6 +17,11 @@ import { LanguageFn } from 'highlight.js'; import { Provider } from '@angular/core'; import { TemplateRef } from '@angular/core'; +// @public +export const KBQ_CODE_BLOCK_DEFAULT_OPTIONS: InjectionToken>; + // @public export const KBQ_CODE_BLOCK_FALLBACK_FILE_LANGUAGE: InjectionToken; @@ -101,6 +106,14 @@ export class KbqCodeBlock implements AfterViewInit { static ɵfac: _angular_core.ɵɵFactoryDeclaration; } +// @public +export type KbqCodeBlockDefaultOptions = Partial<{ + alwaysShowActionbar: boolean; +}>; + +// @public +export const kbqCodeBlockDefaultOptionsProvider: (options: KbqCodeBlockDefaultOptions) => Provider; + // @public export const kbqCodeBlockFallbackFileLanguageProvider: (language: string) => Provider;