diff --git a/libs/core/avatar-group/avatar-group.component.html b/libs/core/avatar-group/avatar-group.component.html index 1507953d8f5..450e1a2edc0 100644 --- a/libs/core/avatar-group/avatar-group.component.html +++ b/libs/core/avatar-group/avatar-group.component.html @@ -14,7 +14,7 @@ (resized)="_detectChanges()" > @for (item of _avatars; track item) { - + } @@ -52,9 +54,17 @@ } @else { - - + + - + = null; + /** @hidden */ @ViewChildren(AvatarGroupItemRendererDirective) _avatarRenderers: QueryList; + /** @hidden */ + @ViewChild(DefaultAvatarGroupOverflowBodyComponent) + defaultAvatarGroupOverflowBody: DefaultAvatarGroupOverflowBodyComponent; + /** @hidden */ @ContentChildren(AvatarGroupItemDirective) _avatars: QueryList; @@ -112,6 +132,9 @@ export class AvatarGroupComponent implements AvatarGroupHostConfig { @ContentChild(AvatarGroupOverflowBodyDirective) _avatarGroupPopoverBody: AvatarGroupOverflowBodyDirective; + /** @hidden */ + opened = signal(false); + /** @hidden */ _contentDirection$ = computed(() => (this._rtlService?.rtlSignal() ? 'rtl' : 'ltr')); @@ -121,6 +144,19 @@ export class AvatarGroupComponent implements AvatarGroupHostConfig { /** @hidden */ private readonly _rtlService = inject(RtlService, { optional: true }); + /** @hidden */ + private _popoverService = inject(PopoverService); + + /** @hidden */ + ngOnInit(): void { + this._popoverService._forceFocus.set(true); + } + + /** @hidden */ + handlePopoverOpen($event: boolean): void { + this.opened.set($event); + } + /** @hidden */ _detectChanges(): void { this._cdr.detectChanges(); diff --git a/libs/core/avatar-group/components/avatar-group-host.component.ts b/libs/core/avatar-group/components/avatar-group-host.component.ts index 5709a8d3b2c..33afbfd36cb 100644 --- a/libs/core/avatar-group/components/avatar-group-host.component.ts +++ b/libs/core/avatar-group/components/avatar-group-host.component.ts @@ -72,6 +72,12 @@ export class AvatarGroupHostComponent @Input() items: QueryList; + /** + * The maximum number of visible avatar items. + **/ + @Input() + maxVisibleItems: Nullable = null; + /** * @hidden * The portals to be rendered in the avatar group. @@ -166,12 +172,18 @@ export class AvatarGroupHostComponent continue; } accWidth += item.width; - if (accWidth <= containerWidth) { + if (accWidth <= containerWidth && (!this.maxVisibleItems || visibleItems.length < this.maxVisibleItems)) { visibleItems.push(item); } else if (!item.forceVisibility) { hiddenItems.push(item); } } + + // Ensure we don't exceed the maxVisibleItems limit + if (this.maxVisibleItems && visibleItems.length > this.maxVisibleItems) { + hiddenItems.unshift(...visibleItems.splice(this.maxVisibleItems)); + } + /* take last item from the visibleItems which is not forced to be visible and push it to the hiddenItems * This is done to free up the space for the overflow button */ diff --git a/libs/core/avatar-group/components/avatar-group-overflow-button.component.ts b/libs/core/avatar-group/components/avatar-group-overflow-button.component.ts index aee4e284aca..59db07b33a1 100644 --- a/libs/core/avatar-group/components/avatar-group-overflow-button.component.ts +++ b/libs/core/avatar-group/components/avatar-group-overflow-button.component.ts @@ -2,7 +2,6 @@ import { ChangeDetectionStrategy, Component, forwardRef, - HostBinding, inject, Input, OnChanges, @@ -29,7 +28,8 @@ import { AVATAR_GROUP_HOST_CONFIG } from '../tokens'; selector: 'fd-avatar-group-overflow-button', template: ` `, host: { - role: 'button' + role: 'button', + class: 'fd-avatar--circle' }, changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.None, @@ -51,13 +51,6 @@ export class AvatarGroupOverflowButtonComponent @Input() size: Size = 'l'; - /** - * Whether the overflow button should be displayed as a circle. - */ - @Input() - @HostBinding('class.fd-avatar--circle') - circle = false; - /** * A number from 1 to 10 representing the background color of the Avatar. */ diff --git a/libs/core/avatar-group/components/default-avatar-group-overflow-body/default-avatar-group-overflow-body.component.html b/libs/core/avatar-group/components/default-avatar-group-overflow-body/default-avatar-group-overflow-body.component.html index 0663667c718..4b3987cc0d0 100644 --- a/libs/core/avatar-group/components/default-avatar-group-overflow-body/default-avatar-group-overflow-body.component.html +++ b/libs/core/avatar-group/components/default-avatar-group-overflow-body/default-avatar-group-overflow-body.component.html @@ -27,11 +27,11 @@ navigationDirection="horizontal" > @if (_overflowPopoverStage === 'main') { - @for (item of avatars; track item) { + @for (avatar of avatars(); track avatar) { diff --git a/libs/core/avatar-group/components/default-avatar-group-overflow-body/default-avatar-group-overflow-body.component.ts b/libs/core/avatar-group/components/default-avatar-group-overflow-body/default-avatar-group-overflow-body.component.ts index 0abde1745eb..f70e2156303 100644 --- a/libs/core/avatar-group/components/default-avatar-group-overflow-body/default-avatar-group-overflow-body.component.ts +++ b/libs/core/avatar-group/components/default-avatar-group-overflow-body/default-avatar-group-overflow-body.component.ts @@ -5,13 +5,16 @@ import { ChangeDetectionStrategy, ChangeDetectorRef, Component, + EventEmitter, Input, OnDestroy, + Output, QueryList, Renderer2, ViewChildren, ViewEncapsulation, - inject + inject, + input } from '@angular/core'; import { FocusableListDirective, RtlService, elementClick$ } from '@fundamental-ngx/cdk/utils'; import { BarModule } from '@fundamental-ngx/core/bar'; @@ -42,18 +45,16 @@ import { AvatarGroupItemDirective } from '../../directives/avatar-group-item.dir changeDetection: ChangeDetectionStrategy.OnPush }) export class DefaultAvatarGroupOverflowBodyComponent implements AfterViewInit, OnDestroy { - /** - * List of avatars to be rendered in the overflow popover. - **/ - @Input() - avatars: Iterable = []; - /** * Title of the overflow popover. * */ @Input() overflowPopoverTitle: string; + /** @hidden */ + @Output() + back = new EventEmitter(); + /** @hidden */ @ViewChildren(AvatarGroupItemRendererDirective) _avatarGroupItemPortals: QueryList; @@ -73,6 +74,12 @@ export class DefaultAvatarGroupOverflowBodyComponent implements AfterViewInit, O get isRtl(): boolean { return !!this._rtlService?.rtl.value; } + + /** + * List of avatars to be rendered in the overflow popover. + **/ + avatars = input([]); + /** @hidden */ private _itemClickSubscription: Subscription; @@ -122,5 +129,6 @@ export class DefaultAvatarGroupOverflowBodyComponent implements AfterViewInit, O _openOverflowMain(): void { this._overflowPopoverStage = 'main'; this._changeDetectorRef.detectChanges(); + this.back.emit(); } } diff --git a/libs/core/avatar-group/directives/avatar-group-item-renderer.directive.ts b/libs/core/avatar-group/directives/avatar-group-item-renderer.directive.ts index b7fe2097d1c..8b489d11891 100644 --- a/libs/core/avatar-group/directives/avatar-group-item-renderer.directive.ts +++ b/libs/core/avatar-group/directives/avatar-group-item-renderer.directive.ts @@ -1,6 +1,7 @@ import { CdkPortalOutlet, TemplatePortal } from '@angular/cdk/portal'; -import { Directive, EmbeddedViewRef, Input, OnInit, ViewContainerRef, inject } from '@angular/core'; +import { Directive, EmbeddedViewRef, Input, OnInit, ViewContainerRef, effect, inject } from '@angular/core'; import { FDK_FOCUSABLE_ITEM_DIRECTIVE, FocusableItem, Nullable } from '@fundamental-ngx/cdk/utils'; +import { AvatarGroupComponent } from '@fundamental-ngx/core/avatar-group'; import { BehaviorSubject, Observable, fromEvent } from 'rxjs'; import { filter, switchMap } from 'rxjs/operators'; import { AVATAR_GROUP_HOST_CONFIG } from '../tokens'; @@ -118,6 +119,11 @@ export class AvatarGroupItemRendererDirective implements OnInit, FocusableItem { filter((element) => !!element), switchMap((element) => fromEvent(element as unknown as HTMLElement, 'keydown') as Observable) ); + effect(() => { + const config = this._hostConfig as AvatarGroupComponent; + this.setTabbable(config.type === 'individual' || config.opened()); + this._isFocusable = config.type === 'individual' || config.opened(); + }); } /** @hidden */ @@ -147,8 +153,6 @@ export class AvatarGroupItemRendererDirective implements OnInit, FocusableItem { this._embeddedViewRef = this._portalOutlet.attach(this._templatePortal); this._embeddedViewRef.detectChanges(); this._element$.next(this.element); - this.setTabbable(this._hostConfig.type === 'individual'); - this._isFocusable = this._hostConfig.type === 'individual'; } /** diff --git a/libs/core/popover/popover-body/popover-body.component.ts b/libs/core/popover/popover-body/popover-body.component.ts index b441f56af3a..f8296d6e63d 100644 --- a/libs/core/popover/popover-body/popover-body.component.ts +++ b/libs/core/popover/popover-body/popover-body.component.ts @@ -5,6 +5,7 @@ import { Component, ElementRef, HostListener, + input, Input, Renderer2, TemplateRef, @@ -67,6 +68,9 @@ export class PopoverBodyComponent implements AfterViewInit { @ViewChild(ScrollbarDirective) _scrollbar: ScrollbarDirective; + /** @hidden */ + focusFirstTabbable = input(false); + /** Whether to wrap content with fd-scrollbar directive. */ _disableScrollbar = false; @@ -150,6 +154,9 @@ export class PopoverBodyComponent implements AfterViewInit { // Call the focus logic if clicked outside the popover this._focusFirstTabbableElement(); } + if (this.focusFirstTabbable()) { + this._focusFirstTabbableElement(true); + } } /** @hidden */ diff --git a/libs/core/popover/popover-service/popover.service.ts b/libs/core/popover/popover-service/popover.service.ts index 9b2f6ce6c39..dde260167a7 100644 --- a/libs/core/popover/popover-service/popover.service.ts +++ b/libs/core/popover/popover-service/popover.service.ts @@ -17,7 +17,8 @@ import { Renderer2, TemplateRef, ViewContainerRef, - inject + inject, + signal } from '@angular/core'; import { Observable, Subject, merge } from 'rxjs'; import { distinctUntilChanged, filter, startWith, takeUntil } from 'rxjs/operators'; @@ -46,6 +47,9 @@ export class PopoverService extends BasePopoverClass { /** Template content displayed inside popover body */ templateContent: Nullable>; + /** Whether to focus the first item on space key press */ + _forceFocus = signal(false); + /** @hidden */ _onLoad = new Subject(); @@ -127,6 +131,13 @@ export class PopoverService extends BasePopoverClass { this._removeOverlay(this._modalBodyClass, this._modalTriggerClass); } }); + + // Add keydown listener for space key + this._renderer.listen('document', 'keydown', (event: KeyboardEvent) => { + if (event.code === 'Space' && this.isOpen) { + this._storeAndFocusFirstTabbable(); + } + }); } /** @@ -588,11 +599,16 @@ export class PopoverService extends BasePopoverClass { /** @hidden */ private _focusFirstTabbableElement(focusLastElement = true): void { if (focusLastElement && this.focusAutoCapture) { - this._lastActiveElement = document.activeElement; - this._getPopoverBody()?._focusFirstTabbableElement(); + this._storeAndFocusFirstTabbable(); } } + /** @hidden */ + private _storeAndFocusFirstTabbable(): void { + this._lastActiveElement = document.activeElement; + this._getPopoverBody()?._focusFirstTabbableElement(true); + } + /** @hidden */ private _focusLastActiveElementBeforeOpen(focusLastElement = true): void { if (focusLastElement && this.restoreFocusOnClose && this.focusAutoCapture && this._lastActiveElement) { diff --git a/libs/docs/core/avatar-group/avatar-group-docs.component.html b/libs/docs/core/avatar-group/avatar-group-docs.component.html index 052f436c220..7471fe5ff03 100644 --- a/libs/docs/core/avatar-group/avatar-group-docs.component.html +++ b/libs/docs/core/avatar-group/avatar-group-docs.component.html @@ -9,7 +9,6 @@ - Group Type Standard Usage

diff --git a/libs/docs/core/avatar-group/examples/group-type/group-type-example.component.html b/libs/docs/core/avatar-group/examples/group-type/group-type-example.component.html index a774f75f4a5..616ef4577fa 100644 --- a/libs/docs/core/avatar-group/examples/group-type/group-type-example.component.html +++ b/libs/docs/core/avatar-group/examples/group-type/group-type-example.component.html @@ -1,3 +1,4 @@ + Standard Avatar group @for (person of people; track person) { @if (!person.imageUrl && !person.glyph) { @@ -70,8 +71,215 @@ > } {{ person.firstName }} {{ person.lastName }} + >{{ person.firstName }} {{ person.lastName }} + + {{ person.position }} + + + Contact Details + + Phone + + {{ person.phone }} + + + + Mobile + + {{ person.mobile }} + + + + Email + + {{ person.email }} + + + + + + } + + +
+ Avatar group with defined size of visible items + + @for (person of people; track person) { + @if (!person.imageUrl && !person.glyph) { + + } + @if (person.imageUrl) { + + } + @if (person.glyph) { + + } + + + + @if (!person.imageUrl && !person.glyph) { + + } + @if (person.imageUrl) { + + } + @if (person.glyph) { + + } + {{ person.firstName }} {{ person.lastName }} + + {{ person.position }} + + + Contact Details + + Phone + + {{ person.phone }} + + + + Mobile + + {{ person.mobile }} + + + + Email + + {{ person.email }} + + + + + + } + +
+ Avatar group with placement + + @for (person of people; track person) { + @if (!person.imageUrl && !person.glyph) { + + } + @if (person.imageUrl) { + + } + @if (person.glyph) { + + } + + + + @if (!person.imageUrl && !person.glyph) { + + } + @if (person.imageUrl) { + + } + @if (person.glyph) { + + } + {{ person.firstName }} {{ person.lastName }} + {{ person.position }} diff --git a/libs/docs/core/avatar-group/examples/group-type/group-type-example.component.ts b/libs/docs/core/avatar-group/examples/group-type/group-type-example.component.ts index 1a61d696292..2ad9f068399 100644 --- a/libs/docs/core/avatar-group/examples/group-type/group-type-example.component.ts +++ b/libs/docs/core/avatar-group/examples/group-type/group-type-example.component.ts @@ -7,13 +7,21 @@ import { AvatarGroupComponent, AvatarGroupItemDirective } from '@fundamental-ngx import { LinkComponent } from '@fundamental-ngx/core/link'; import { PopoverComponent } from '@fundamental-ngx/core/popover'; import { QuickViewModule } from '@fundamental-ngx/core/quick-view'; +import { DescriptionComponent } from '@fundamental-ngx/docs/shared'; import { AvatarGroupDataExampleService } from '../avatar-group-data-example.service'; @Component({ selector: 'fd-avatar-group-group-type-example', templateUrl: './group-type-example.component.html', standalone: true, - imports: [AvatarGroupComponent, AvatarComponent, QuickViewModule, LinkComponent, AvatarGroupItemDirective] + imports: [ + AvatarGroupComponent, + AvatarComponent, + QuickViewModule, + LinkComponent, + AvatarGroupItemDirective, + DescriptionComponent + ] }) export class GroupTypeExampleComponent { size: Size = 'l'; diff --git a/libs/docs/platform/vhd/examples/platform-vhd-multi-input-example.component.html b/libs/docs/platform/vhd/examples/platform-vhd-multi-input-example.component.html index d2618098b2b..2f135dda30f 100644 --- a/libs/docs/platform/vhd/examples/platform-vhd-multi-input-example.component.html +++ b/libs/docs/platform/vhd/examples/platform-vhd-multi-input-example.component.html @@ -12,7 +12,6 @@