diff --git a/libs/core/list/directives/list-title.directive.spec.ts b/libs/core/list/directives/list-title.directive.spec.ts index 097b114b74f..7119cb6895c 100644 --- a/libs/core/list/directives/list-title.directive.spec.ts +++ b/libs/core/list/directives/list-title.directive.spec.ts @@ -1,20 +1,25 @@ import { Component, ElementRef, ViewChild } from '@angular/core'; import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; +import { By } from '@angular/platform-browser'; import { ListModule } from '../list.module'; +import { ListTitleDirective } from './list-title.directive'; @Component({ - template: `
  • ListTitleComponent
  • `, + template: `
  • ListTitleComponent
  • `, standalone: true, imports: [ListModule] }) class TestComponent { @ViewChild('componentElement', { read: ElementRef }) - ref: ElementRef; + ref!: ElementRef; + + isTruncate = false; } -describe('ListTitleComponent', () => { +describe('ListTitleDirective', () => { let component: TestComponent; let fixture: ComponentFixture; + let directiveInstance: ListTitleDirective; beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ @@ -26,13 +31,28 @@ describe('ListTitleComponent', () => { fixture = TestBed.createComponent(TestComponent); component = fixture.componentInstance; fixture.detectChanges(); + + const debugElement = fixture.debugElement.query(By.directive(ListTitleDirective)); + directiveInstance = debugElement.injector.get(ListTitleDirective); }); it('should create', () => { expect(component).toBeTruthy(); + expect(directiveInstance).toBeTruthy(); + }); + + it('should assign base class', () => { + expect(component.ref.nativeElement.classList).toContain('fd-list__title'); }); - it('should assign class', () => { - expect(component.ref.nativeElement.className).toBe('fd-list__title'); + it('should not have truncate or wrap classes by default', () => { + expect(component.ref.nativeElement.classList).not.toContain('fd-list__title--truncate'); + }); + + it('should add truncate class when truncate is true (via input binding)', () => { + component.isTruncate = true; + fixture.detectChanges(); + + expect(component.ref.nativeElement.classList).toContain('fd-list__title--truncate'); }); }); diff --git a/libs/core/list/directives/list-title.directive.ts b/libs/core/list/directives/list-title.directive.ts index 0ce1f422886..87bf5675be4 100644 --- a/libs/core/list/directives/list-title.directive.ts +++ b/libs/core/list/directives/list-title.directive.ts @@ -1,18 +1,22 @@ -import { Directive, ElementRef, HostBinding, Input, OnInit } from '@angular/core'; +import { booleanAttribute, Directive, ElementRef, input, OnInit } from '@angular/core'; @Directive({ selector: '[fd-list-title], [fdListTitle]', - standalone: true + standalone: true, + host: { + class: 'fd-list__title', + '[class.fd-list__title--truncate]': 'truncate()' + } }) export class ListTitleDirective implements OnInit { - /** @hidden */ - @HostBinding('class.fd-list__title') - fdListTitleClass = true; + /** + * @deprecated + * Whether or not this should be wrapped, when too much text. + */ + wrap = input(false, { transform: booleanAttribute }); - /** Whether or not this should be wrapped, when too much text. */ - @Input() - @HostBinding('class.fd-list__title--wrap') - wrap = false; + /** Whether the text should truncate with ellipsis. */ + truncate = input(false, { transform: booleanAttribute }); /** @hidden */ constructor(public elRef: ElementRef) {} diff --git a/libs/core/list/directives/subline/list-subline.directive.spec.ts b/libs/core/list/directives/subline/list-subline.directive.spec.ts index 7058020afa1..2edd3ed37dd 100644 --- a/libs/core/list/directives/subline/list-subline.directive.spec.ts +++ b/libs/core/list/directives/subline/list-subline.directive.spec.ts @@ -1,20 +1,24 @@ import { Component, ElementRef, ViewChild } from '@angular/core'; import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; +import { By } from '@angular/platform-browser'; import { ListSublineDirective } from './list-subline.directive'; @Component({ - template: `
  • List Subline Directive Test
  • `, + template: `
  • List Subline Directive Test
  • `, standalone: true, imports: [ListSublineDirective] }) class TestComponent { @ViewChild('componentElement', { read: ElementRef }) - ref: ElementRef; + ref!: ElementRef; + + isTruncate = false; } describe('ListSublineDirective', () => { let component: TestComponent; let fixture: ComponentFixture; + let directiveInstance: ListSublineDirective; beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ @@ -26,13 +30,29 @@ describe('ListSublineDirective', () => { fixture = TestBed.createComponent(TestComponent); component = fixture.componentInstance; fixture.detectChanges(); + + // Get the directive instance + const debugElement = fixture.debugElement.query(By.directive(ListSublineDirective)); + directiveInstance = debugElement.injector.get(ListSublineDirective); }); it('should create', () => { expect(component).toBeTruthy(); + expect(directiveInstance).toBeTruthy(); + }); + + it('should assign base class', () => { + expect(component.ref.nativeElement.classList).toContain('fd-list__subline'); }); - it('should assign class', () => { - expect(component.ref.nativeElement.className).toBe('fd-list__subline'); + it('should not have truncate class by default', () => { + expect(component.ref.nativeElement.classList).not.toContain('fd-list__subline--truncate'); + }); + + it('should add truncate class when truncate is true (via input binding)', () => { + component.isTruncate = true; + fixture.detectChanges(); + + expect(component.ref.nativeElement.classList).toContain('fd-list__subline--truncate'); }); }); diff --git a/libs/core/list/directives/subline/list-subline.directive.ts b/libs/core/list/directives/subline/list-subline.directive.ts index 9d00d9a8747..59b44805efc 100644 --- a/libs/core/list/directives/subline/list-subline.directive.ts +++ b/libs/core/list/directives/subline/list-subline.directive.ts @@ -1,10 +1,14 @@ -import { Directive } from '@angular/core'; +import { booleanAttribute, Directive, input } from '@angular/core'; @Directive({ selector: '[fdListSubline], [fd-list-subline]', host: { - class: 'fd-list__subline' + class: 'fd-list__subline', + '[class.fd-list__subline--truncate]': 'truncate()' }, standalone: true }) -export class ListSublineDirective {} +export class ListSublineDirective { + /** Whether the text should truncate with ellipsis. */ + truncate = input(false, { transform: booleanAttribute }); +} diff --git a/libs/core/list/list.component.scss b/libs/core/list/list.component.scss index a513e4aac40..d2c37c9aa86 100644 --- a/libs/core/list/list.component.scss +++ b/libs/core/list/list.component.scss @@ -19,3 +19,59 @@ .fd-list.fd-list--selection .fd-radio:focus + .fd-radio__label { outline: none; } + +/* + * TO BE REMOVED WITH FUNDAMENTAL STYLES VERSION 0.41.0 + * ---------------------------------------------------------------------------------------------------- + */ + +.fd-list--subline .fd-list__item { + height: auto; +} + +.fd-list--subline .fd-list__title { + width: 100%; + max-width: 100%; + font-style: normal; + line-height: normal; + white-space: normal; + font-size: var(--sapFontSize); + color: var(--sapList_TextColor); + font-family: var(--sapFontSemiboldDuplexFamily); +} + +.fd-list--subline .fd-list__title--truncate { + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.fd-list--subline .fd-list__subline { + font-size: var(--sapFontSize); + line-height: normal; + color: var(--sapTextColor); + font-family: var(--sapFontFamily); + font-weight: normal; + -webkit-box-sizing: border-box; + box-sizing: border-box; + forced-color-adjust: none; + padding-inline: 0; + padding-block: 0; + margin-inline: 0; + margin-block: 0; + border: 0; + width: 100%; + max-width: 100%; + font-style: normal; + line-height: normal; + white-space: normal; + font-size: var(--sapFontSize); + font-family: var(--sapFontFamily); + color: var(--sapContent_LabelColor); +} + +.fd-list--subline .fd-list__subline--truncate { + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} diff --git a/libs/core/user-menu/components/user-menu-list-item.component.ts b/libs/core/user-menu/components/user-menu-list-item.component.ts index efc6ae67196..8a9b09c3192 100644 --- a/libs/core/user-menu/components/user-menu-list-item.component.ts +++ b/libs/core/user-menu/components/user-menu-list-item.component.ts @@ -144,7 +144,7 @@ export class UserMenuListItemComponent implements KeyboardSupportItemInterface { this.isOpenChange.emit(isOpen); this._onZoneStable().subscribe(() => { - this.isOpen() ? linkElement.classList.add('is-active') : linkElement.classList.remove('is-active'); + this.isOpen() ? linkElement.classList.add('is-selected') : linkElement.classList.remove('is-selected'); firstTabbableElement.focus(); }); diff --git a/libs/core/user-menu/directives/user-menu-subline.directive.spec.ts b/libs/core/user-menu/directives/user-menu-subline.directive.spec.ts index d8fd184dba4..5e107b7cae1 100644 --- a/libs/core/user-menu/directives/user-menu-subline.directive.spec.ts +++ b/libs/core/user-menu/directives/user-menu-subline.directive.spec.ts @@ -5,15 +5,18 @@ import { By } from '@angular/platform-browser'; import { UserMenuSublineDirective } from './user-menu-subline.directive'; @Component({ - template: `User Menu Subline Test`, + template: `User Menu Subline Test`, standalone: true, imports: [UserMenuSublineDirective] }) -class TestComponent {} +class TestComponent { + isTruncate = false; +} describe('UserMenuSublineDirective', () => { let fixture: ComponentFixture; let debugElement: DebugElement; + let directiveInstance: UserMenuSublineDirective; beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ @@ -23,17 +26,29 @@ describe('UserMenuSublineDirective', () => { beforeEach(() => { fixture = TestBed.createComponent(TestComponent); - fixture.detectChanges(); debugElement = fixture.debugElement.query(By.directive(UserMenuSublineDirective)); + directiveInstance = debugElement.injector.get(UserMenuSublineDirective); }); it('should create', () => { expect(fixture).toBeTruthy(); + expect(directiveInstance).toBeTruthy(); + }); + + it('should add base class to host', () => { + expect(debugElement.nativeElement.classList).toContain('fd-user-menu__subline'); }); - it('should add class to host', () => { - expect(debugElement.nativeElement.className.includes('fd-user-menu__subline')).toBe(true); + it('should not have truncate class by default', () => { + expect(debugElement.nativeElement.classList).not.toContain('fd-user-menu__subline--truncate'); + }); + + it('should add truncate class when truncate is true (via input binding)', () => { + fixture.componentInstance.isTruncate = true; + fixture.detectChanges(); + + expect(debugElement.nativeElement.classList).toContain('fd-user-menu__subline--truncate'); }); }); diff --git a/libs/core/user-menu/directives/user-menu-subline.directive.ts b/libs/core/user-menu/directives/user-menu-subline.directive.ts index e178a060b16..20717ead0f2 100644 --- a/libs/core/user-menu/directives/user-menu-subline.directive.ts +++ b/libs/core/user-menu/directives/user-menu-subline.directive.ts @@ -1,10 +1,14 @@ -import { Directive } from '@angular/core'; +import { booleanAttribute, Directive, input } from '@angular/core'; @Directive({ // eslint-disable-next-line @angular-eslint/directive-selector selector: '[fd-user-menu-subline]', host: { - class: 'fd-user-menu__subline' + class: 'fd-user-menu__subline', + '[class.fd-user-menu__subline--truncate]': 'truncate()' } }) -export class UserMenuSublineDirective {} +export class UserMenuSublineDirective { + /** Whether the text should truncate with ellipsis. */ + truncate = input(false, { transform: booleanAttribute }); +} diff --git a/libs/core/user-menu/directives/user-menu-user-name.directive.spec.ts b/libs/core/user-menu/directives/user-menu-user-name.directive.spec.ts index 01eeb1bd2ef..9b1c2644a39 100644 --- a/libs/core/user-menu/directives/user-menu-user-name.directive.spec.ts +++ b/libs/core/user-menu/directives/user-menu-user-name.directive.spec.ts @@ -5,15 +5,18 @@ import { By } from '@angular/platform-browser'; import { UserMenuUserNameDirective } from './user-menu-user-name.directive'; @Component({ - template: `User Menu User Name Test`, + template: `User Menu User Name Test`, standalone: true, imports: [UserMenuUserNameDirective] }) -class TestComponent {} +class TestComponent { + isTruncate = false; +} describe('UserMenuUserNameDirective', () => { let fixture: ComponentFixture; let debugElement: DebugElement; + let directiveInstance: UserMenuUserNameDirective; beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ @@ -23,17 +26,29 @@ describe('UserMenuUserNameDirective', () => { beforeEach(() => { fixture = TestBed.createComponent(TestComponent); - fixture.detectChanges(); debugElement = fixture.debugElement.query(By.directive(UserMenuUserNameDirective)); + directiveInstance = debugElement.injector.get(UserMenuUserNameDirective); }); it('should create', () => { expect(fixture).toBeTruthy(); + expect(directiveInstance).toBeTruthy(); + }); + + it('should add base class to host', () => { + expect(debugElement.nativeElement.classList).toContain('fd-user-menu__user-name'); }); - it('should add class to host', () => { - expect(debugElement.nativeElement.className.includes('fd-user-menu__user-name')).toBe(true); + it('should not have truncate class by default', () => { + expect(debugElement.nativeElement.classList).not.toContain('fd-user-menu__user-name--truncate'); + }); + + it('should add truncate class when truncate is true (via input binding)', () => { + fixture.componentInstance.isTruncate = true; + fixture.detectChanges(); + + expect(debugElement.nativeElement.classList).toContain('fd-user-menu__user-name--truncate'); }); }); diff --git a/libs/core/user-menu/directives/user-menu-user-name.directive.ts b/libs/core/user-menu/directives/user-menu-user-name.directive.ts index 59d31aa65e2..7831a97b8db 100644 --- a/libs/core/user-menu/directives/user-menu-user-name.directive.ts +++ b/libs/core/user-menu/directives/user-menu-user-name.directive.ts @@ -1,10 +1,14 @@ -import { Directive } from '@angular/core'; +import { booleanAttribute, Directive, input } from '@angular/core'; @Directive({ // eslint-disable-next-line @angular-eslint/directive-selector selector: '[fd-user-menu-user-name]', host: { - class: 'fd-user-menu__user-name' + class: 'fd-user-menu__user-name', + '[class.fd-user-menu__user-name--truncate]': 'truncate()' } }) -export class UserMenuUserNameDirective {} +export class UserMenuUserNameDirective { + /** Whether the text should truncate with ellipsis. */ + truncate = input(false, { transform: booleanAttribute }); +} diff --git a/libs/core/user-menu/user-menu.component.html b/libs/core/user-menu/user-menu.component.html index 17da848ee88..fa0971a33a9 100644 --- a/libs/core/user-menu/user-menu.component.html +++ b/libs/core/user-menu/user-menu.component.html @@ -9,7 +9,11 @@ - + + @if (!isUserNameVisible()) { + + } +
    @@ -44,6 +48,21 @@ + +
    +
    +
    + +
    +
    +
    +
    +
    +
    + diff --git a/libs/core/user-menu/user-menu.component.scss b/libs/core/user-menu/user-menu.component.scss index a01e85e0225..f13dd66f63a 100644 --- a/libs/core/user-menu/user-menu.component.scss +++ b/libs/core/user-menu/user-menu.component.scss @@ -1,7 +1,426 @@ -@import 'fundamental-styles/dist/user-menu.css'; +// @import 'fundamental-styles/dist/user-menu.css'; @import 'fundamental-styles/dist/menu.css'; -// Specific to Angular Implementation (composite components) +/* + * TO BE REMOVED WITH FUNDAMENTAL STYLES VERSION 0.41.0 + * ---------------------------------------------------------------------------------------------------- + */ + +.fd-user-menu { + background: var(--sapGroup_ContentBackground); +} +.fd-user-menu .fd-user-menu__title-bar { + --fdBar_Shadow: none; +} +.fd-user-menu .fd-panel__header:has(.fd-panel__button[aria-expanded='false']) { + --fdPanel_Header_Border_Bottom_Left_Radius: 0; + --fdPanel_Header_Border_Bottom_Right_Radius: 0; +} +.fd-user-menu .fd-user-menu__popover-wrapper { + overflow: visible; + width: 20rem; + min-width: 20rem; +} +.fd-user-menu__body { + font-size: var(--sapFontSize); + line-height: normal; + color: var(--sapTextColor); + font-family: var(--sapFontFamily); + font-weight: normal; + -webkit-box-sizing: border-box; + box-sizing: border-box; + forced-color-adjust: none; + padding-inline: 0; + padding-block: 0; + margin-inline: 0; + margin-block: 0; + border: 0; + display: -webkit-box; + display: -ms-flexbox; + display: flex; + -webkit-box-orient: vertical; + -webkit-box-direction: normal; + -ms-flex-direction: column; + flex-direction: column; + gap: 0.5rem; + overflow: visible; + min-width: 18rem; + max-width: 20rem; + position: relative; + padding-inline: 0.5rem; + padding-block: 2.5rem 0.5rem; +} +.fd-user-menu__body::before, +.fd-user-menu__body::after { + -webkit-box-sizing: inherit; + box-sizing: inherit; + font-size: inherit; +} +.fd-user-menu__body:has(.fd-user-menu__details-view) { + padding: 0; +} +.fd-user-menu__header { + font-size: var(--sapFontSize); + line-height: normal; + color: var(--sapTextColor); + font-family: var(--sapFontFamily); + font-weight: normal; + -webkit-box-sizing: border-box; + box-sizing: border-box; + forced-color-adjust: none; + padding-inline: 0; + padding-block: 0; + margin-inline: 0; + margin-block: 0; + border: 0; + display: -webkit-box; + display: -ms-flexbox; + display: flex; + -webkit-box-orient: vertical; + -webkit-box-direction: normal; + -ms-flex-direction: column; + flex-direction: column; + gap: 0.5rem; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; +} +.fd-user-menu__header::before, +.fd-user-menu__header::after { + -webkit-box-sizing: inherit; + box-sizing: inherit; + font-size: inherit; +} +.fd-user-menu__header-container, +.fd-user-menu__subheader { + font-size: var(--sapFontSize); + line-height: normal; + color: var(--sapTextColor); + font-family: var(--sapFontFamily); + font-weight: normal; + -webkit-box-sizing: border-box; + box-sizing: border-box; + forced-color-adjust: none; + padding-inline: 0; + padding-block: 0; + margin-inline: 0; + margin-block: 0; + border: 0; + display: -webkit-box; + display: -ms-flexbox; + display: flex; + -webkit-box-orient: vertical; + -webkit-box-direction: normal; + -ms-flex-direction: column; + flex-direction: column; + gap: 0.25rem; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + max-width: 100%; + text-align: center; + -webkit-margin-after: 0.5rem; + margin-block-end: 0.5rem; +} +.fd-user-menu__header-container::before, +.fd-user-menu__header-container::after, +.fd-user-menu__subheader::before, +.fd-user-menu__subheader::after { + -webkit-box-sizing: inherit; + box-sizing: inherit; + font-size: inherit; +} +.fd-user-menu__user-name { + font-size: var(--sapFontSize); + line-height: normal; + color: var(--sapTextColor); + font-family: var(--sapFontFamily); + font-weight: normal; + -webkit-box-sizing: border-box; + box-sizing: border-box; + forced-color-adjust: none; + padding-inline: 0; + padding-block: 0; + margin-inline: 0; + margin-block: 0; + border: 0; + max-width: 100%; + line-height: 1.4rem; + white-space: normal; + color: var(--sapTextColor); + font-size: var(--sapFontLargeSize); + font-family: var(--sapFontSemiboldDuplexFamily); +} +.fd-user-menu__user-name::before, +.fd-user-menu__user-name::after { + -webkit-box-sizing: inherit; + box-sizing: inherit; + font-size: inherit; +} +.fd-user-menu__user-name--truncate { + display: -webkit-box; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; + overflow: hidden; + white-space: normal; + text-overflow: ellipsis; + max-height: 2.8rem; +} +.fd-user-menu__user-info, +.fd-user-menu__user-role, +.fd-user-menu__subline { + font-size: var(--sapFontSize); + line-height: normal; + color: var(--sapTextColor); + font-family: var(--sapFontFamily); + font-weight: normal; + -webkit-box-sizing: border-box; + box-sizing: border-box; + forced-color-adjust: none; + padding-inline: 0; + padding-block: 0; + margin-inline: 0; + margin-block: 0; + border: 0; + width: 100%; + max-width: 100%; + white-space: normal; + color: var(--sapContent_LabelColor); +} +.fd-user-menu__user-info::before, +.fd-user-menu__user-info::after, +.fd-user-menu__user-role::before, +.fd-user-menu__user-role::after, +.fd-user-menu__subline::before, +.fd-user-menu__subline::after { + -webkit-box-sizing: inherit; + box-sizing: inherit; + font-size: inherit; +} +.fd-user-menu__user-info--truncate, +.fd-user-menu__user-role--truncate, +.fd-user-menu__subline--truncate { + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} +.fd-user-menu .fd-user-menu__menu { + width: 100%; +} +.fd-user-menu .fd-user-menu__menu-list { + -webkit-box-shadow: none; + box-shadow: none; + border-radius: 0; +} +.fd-user-menu .fd-user-menu__menu-list > *:first-child, +.fd-user-menu .fd-user-menu__menu-list > *:last-child { + border-radius: 0; +} +.fd-user-menu .fd-user-menu__menu-list.fd-user-menu__menu-list { + -webkit-box-shadow: none; + box-shadow: none; + border-radius: 0; +} +.fd-user-menu .fd-user-menu__menu-list.fd-user-menu__menu-list > *:first-child, +.fd-user-menu .fd-user-menu__menu-list.fd-user-menu__menu-list > *:last-child { + border-radius: 0; +} +.fd-user-menu .fd-user-menu__panel > div { + border-radius: 0; +} +.fd-user-menu__content-container { + font-size: var(--sapFontSize); + line-height: normal; + color: var(--sapTextColor); + font-family: var(--sapFontFamily); + font-weight: normal; + -webkit-box-sizing: border-box; + box-sizing: border-box; + forced-color-adjust: none; + padding-inline: 0; + padding-block: 0; + margin-inline: 0; + margin-block: 0; + border: 0; + --fdPanel_Margin_Bottom: 0.25rem; +} +.fd-user-menu__content-container::before, +.fd-user-menu__content-container::after { + -webkit-box-sizing: inherit; + box-sizing: inherit; + font-size: inherit; +} +[class*='-condensed'] .fd-user-menu .fd-menu:not([class*='-cozy']), +[class*='-compact'] .fd-user-menu .fd-menu:not([class*='-cozy']), +.fd-user-menu .fd-menu[class*='-condensed'], +.fd-user-menu .fd-menu[class*='-compact'] { + --fdMenu_Icon_Width: 2.5rem; + --fdMenu_Link_Height: 2.5rem; + --fdMenu_Item_Spacing_Left: 0.75rem; + --fdMenu_Item_Spacing_Right: 0.75rem; +} +.fd-user-menu--mobile { + display: -webkit-box; + display: -ms-flexbox; + display: flex; + -webkit-box-orient: vertical; + -webkit-box-direction: normal; + -ms-flex-direction: column; + flex-direction: column; + height: 100%; + -webkit-padding-before: 0; + padding-block-start: 0; +} +.fd-user-menu--mobile .fd-user-menu__body { + -webkit-box-flex: 1; + -ms-flex: 1; + flex: 1; + -webkit-padding-before: 1rem; + padding-block-start: 1rem; +} +.fd-user-menu--tool-header .fd-user-menu__user-name { + font-family: var(--sapFontBlackFamily); + font-size: 1.25rem; + line-height: 1.3; + color: var(-–sapTitleColor); +} +.fd-user-menu--tool-header .fd-user-menu__navigation-submenu { + top: -0.5rem; + border: none; + padding-block: 0.5rem; + padding-inline: 0.5rem; + border-radius: 0.5rem; + -webkit-margin-end: 0.3875rem; + margin-inline-end: 0.3875rem; + -webkit-box-shadow: var(--sapMenu_Shadow1); + box-shadow: var(--sapMenu_Shadow1); + background: var(--sapGroup_ContentBackground); +} +.fd-user-menu--tool-header .fd-user-menu__navigation-submenu-wrapper { + overflow: visible; +} +.fd-user-menu--tool-header .fd-user-menu__user-role { + font-family: var(--sapFontFamily); + font-size: var(--sapFontSize); + line-height: var(--sapContent_LineHeight); + color: var(–sapContent_LabelColor); +} +.fd-user-menu--tool-header .fd-user-menu__body { + overflow: visible; +} +.fd-user-menu--tool-header .fd-user-menu__body:has(.fd-user-menu__navigation-menu:first-child) { + padding-block: 0; +} +.fd-user-menu--tool-header .fd-user-menu__popover-body { + border: none; + padding-block: 0.75rem; + padding-inline: 0; + border-radius: 0.75rem; + -webkit-box-shadow: var(--sapMenu_Shadow2); + box-shadow: var(--sapMenu_Shadow2); + background: var(--sapGroup_ContentBackground); +} +.fd-user-menu--tool-header .fd-user-menu__footer { + font-size: var(--sapFontSize); + line-height: normal; + color: var(--sapTextColor); + font-family: var(--sapFontFamily); + font-weight: normal; + -webkit-box-sizing: border-box; + box-sizing: border-box; + forced-color-adjust: none; + padding-inline: 0; + padding-block: 0; + margin-inline: 0; + margin-block: 0; + border: 0; + display: -webkit-box; + display: -ms-flexbox; + display: flex; + -webkit-box-orient: horizontal; + -webkit-box-direction: normal; + -ms-flex-direction: row; + flex-direction: row; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + -webkit-box-pack: end; + -ms-flex-pack: end; + justify-content: flex-end; + padding-block: 0.75rem; + padding-inline: 1.125rem; +} +.fd-user-menu--tool-header .fd-user-menu__footer::before, +.fd-user-menu--tool-header .fd-user-menu__footer::after { + -webkit-box-sizing: inherit; + box-sizing: inherit; + font-size: inherit; +} +.fd-user-menu--tool-header .fd-navigation__item.fd-navigation__item--title { + --fdNavigation_Item_Title_Display: flex; + -webkit-margin-after: 1rem; + margin-block-end: 1rem; +} + +.fd-user-menu__body .fd-list--subline .fd-list__item { + height: auto; +} + +.fd-user-menu__body .fd-list--subline .fd-list__title { + width: 100%; + max-width: 100%; + font-style: normal; + line-height: normal; + white-space: normal; + font-size: var(--sapFontSize); + color: var(--sapList_TextColor); + font-family: var(--sapFontSemiboldDuplexFamily); +} + +.fd-user-menu__body .fd-list--subline .fd-list__title--truncate { + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.fd-user-menu__body .fd-list--subline .fd-list__subline { + font-size: var(--sapFontSize); + line-height: normal; + color: var(--sapTextColor); + font-family: var(--sapFontFamily); + font-weight: normal; + -webkit-box-sizing: border-box; + box-sizing: border-box; + forced-color-adjust: none; + padding-inline: 0; + padding-block: 0; + margin-inline: 0; + margin-block: 0; + border: 0; + width: 100%; + max-width: 100%; + font-style: normal; + line-height: normal; + white-space: normal; + font-size: var(--sapFontSize); + font-family: var(--sapFontFamily); + color: var(--sapContent_LabelColor); +} + +.fd-user-menu__body .fd-list--subline .fd-list__subline--truncate { + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +/* + * ---------------------------------------------------------------------------------------------------- + */ + +/* + * Specific to Angular Implementation (composite components) + */ + .fd-user-menu { background: transparent; } @@ -70,32 +489,21 @@ fd-user-menu-control:is(:focus) { .fd-user-menu__body { position: relative; + width: 20rem; + min-width: 20rem; + max-width: 20rem; } .fd-user-menu__body:has(.fd-user-menu__details-view) { padding: 0; } -.fd-user-menu__user-name { - display: -webkit-box; - -webkit-box-orient: vertical; - -webkit-line-clamp: 2; - overflow: hidden; - max-width: 100%; - white-space: normal; - line-height: 1.4rem; - max-height: 2.8rem; -} - -.fd-panel__title, -.fd-user-menu__subline { - width: 100%; +.fd-user-menu__header-container { max-width: 100%; - overflow: hidden; - white-space: nowrap; - text-overflow: ellipsis; } -.fd-user-menu__header-container { - max-width: 100%; +.fd-user-menu .fd-popover__body > .fd-scrollbar { + width: 20rem; + min-width: 20rem; + max-width: 20rem; } diff --git a/libs/core/user-menu/user-menu.component.ts b/libs/core/user-menu/user-menu.component.ts index abf83f6d1c8..a12b860ff60 100644 --- a/libs/core/user-menu/user-menu.component.ts +++ b/libs/core/user-menu/user-menu.component.ts @@ -5,16 +5,15 @@ import { ChangeDetectionStrategy, ChangeDetectorRef, Component, - ContentChild, contentChild, contentChildren, + DestroyRef, effect, ElementRef, - EventEmitter, inject, input, OnInit, - Output, + output, Renderer2, signal, TemplateRef, @@ -36,10 +35,13 @@ import { import { PopoverBodyComponent, PopoverComponent, PopoverControlComponent } from '@fundamental-ngx/core/popover'; +import { BarComponent, BarElementDirective, BarMiddleDirective } from '@fundamental-ngx/core/bar'; + import { UserMenuBodyComponent } from './components/user-menu-body.component'; import { UserMenuControlComponent } from './components/user-menu-control.component'; import { UserMenuListItemComponent } from './components/user-menu-list-item.component'; import { UserMenuControlElementDirective } from './directives/user-menu-control-element.directive'; +import { UserMenuUserNameDirective } from './directives/user-menu-user-name.directive'; @Component({ selector: 'fd-user-menu', @@ -52,6 +54,9 @@ import { UserMenuControlElementDirective } from './directives/user-menu-control- }, imports: [ CommonModule, + BarComponent, + BarMiddleDirective, + BarElementDirective, PopoverComponent, PopoverBodyComponent, PopoverControlComponent, @@ -63,31 +68,51 @@ import { UserMenuControlElementDirective } from './directives/user-menu-control- }) export class UserMenuComponent implements OnInit, AfterViewInit { /** Event thrown, when the user menu is opened or closed */ - @Output() - isOpenChange: EventEmitter = new EventEmitter(); + isOpenChange = output(); - /** @hidden */ - @ContentChild(UserMenuControlComponent) - userMenuControl: UserMenuControlComponent; + /** Whether the user menu is in mobile mode */ + mobile = input(false, { transform: booleanAttribute }); - /** @hidden */ - listItems = contentChildren(UserMenuListItemComponent, { descendants: true }); + /** Whether the user menu is open */ + readonly isOpen = signal(false); + + /** + * Signal indicating whether the user name element is currently visible + * within the user menu. This updates automatically as the element + * enters or leaves the viewport. + * + * Used by the template to conditionally render the sticky header. + */ + readonly isUserNameVisible = signal(true); + + /** + * Signal storing the HTML content of the user name element. + * When the original element scrolls out of view, this content + * is displayed in the sticky header. + */ + readonly userNameContent = signal(''); /** @hidden */ - userMenuBody = contentChild(UserMenuBodyComponent, { descendants: true }); + protected readonly userMenuControl = contentChild(UserMenuControlComponent); /** @hidden */ - userMenuControlElement = contentChild(UserMenuControlElementDirective, { descendants: true, read: ElementRef }); + protected readonly userNameEl = contentChild(UserMenuUserNameDirective, { read: ElementRef }); - /** Whether the user menu is in mobile mode */ - mobile = input(false, { transform: booleanAttribute }); + /** @hidden */ + protected readonly userMenuControlElement = contentChild(UserMenuControlElementDirective, { + descendants: true, + read: ElementRef + }); - /** Whether the user menu is open */ - isOpen = signal(false); + /** @hidden */ + protected readonly userMenuBody = contentChild(UserMenuBodyComponent, { descendants: true }); /** @hidden */ protected navigationArrow$: Observable; + /** @hidden */ + private _listItems = contentChildren(UserMenuListItemComponent, { descendants: true }); + /** @hidden */ private _rtlService = inject(RtlService); @@ -103,11 +128,13 @@ export class UserMenuComponent implements OnInit, AfterViewInit { /** @hidden */ private _dialogRef: DialogRef | undefined; + private _destroyRef = inject(DestroyRef); + /** @hidden */ constructor() { effect(() => { const isMobile = this.mobile(); - this.listItems()?.forEach((item) => item.mobile.set(isMobile)); + this._listItems()?.forEach((item) => item.mobile.set(isMobile)); }); } @@ -121,7 +148,42 @@ export class UserMenuComponent implements OnInit, AfterViewInit { /** @hidden */ ngAfterViewInit(): void { const isMobile = this.mobile(); - this.listItems()?.forEach((item) => item.mobile.set(isMobile)); + this._listItems()?.forEach((item) => item.mobile.set(isMobile)); + + const el = this.userNameEl()?.nativeElement; + + if (!el) { + return; + } + + // logic for showing/hiding the popover header with user name on scroll + const intersectionObserver = new IntersectionObserver( + (entries) => { + this.isUserNameVisible.set(entries[0].isIntersecting); + }, + { root: null, threshold: 0.1 } + ); + + intersectionObserver.observe(el); + + // Initialize + this.userNameContent.set(el.innerHTML.trim()); + + // Watch for changes in projected content + const mutationObserver = new MutationObserver(() => { + this.userNameContent.set(el.innerHTML.trim()); + }); + + mutationObserver.observe(el, { + childList: true, + characterData: true, + subtree: true + }); + + this._destroyRef.onDestroy(() => { + intersectionObserver.disconnect(); + mutationObserver.disconnect(); + }); } /** Method that opens the user menu */ @@ -133,13 +195,13 @@ export class UserMenuComponent implements OnInit, AfterViewInit { close(): void { this.isOpenChangeHandle(false); - if (this.listItems().length > 0) { - this.listItems().forEach((item) => { + if (this._listItems().length > 0) { + this._listItems().forEach((item) => { item.isOpen.set(false); item._elementRef?.nativeElement.querySelector('.fd-menu__link')?.classList.remove('is-active'); }); - this.listItems()[0]?._tabIndex$.set(0); + this._listItems()[0]?._tabIndex$.set(0); } this._clearSubmenu(); @@ -159,7 +221,7 @@ export class UserMenuComponent implements OnInit, AfterViewInit { const refSub = this._dialogRef.afterClosed.subscribe({ next: () => { - this.userMenuControl.focus(); + this.userMenuControl()?.focus(); refSub.unsubscribe(); }, error: (type) => { @@ -181,7 +243,7 @@ export class UserMenuComponent implements OnInit, AfterViewInit { this.isOpenChange.emit(isOpen); if (!isOpen && !this.mobile()) { - this.userMenuControl.focus(); + this.userMenuControl()?.focus(); } const userMenuControlEl = this.userMenuControlElement()?.nativeElement; diff --git a/libs/docs/core/list-subline/examples/list-subline-standard-example.component.html b/libs/docs/core/list-subline/examples/list-subline-standard-example.component.html index 80002d31e5a..11baee34737 100644 --- a/libs/docs/core/list-subline/examples/list-subline-standard-example.component.html +++ b/libs/docs/core/list-subline/examples/list-subline-standard-example.component.html @@ -17,9 +17,41 @@
  • -
    Title
    -
    Subline Text
    -
    Subline Text
    +
    + Title Lorem ipsum dolor sit amet, consectetur adipisicing elit. Voluptas, deserunt. Consectetur, aliquid + tempore! Aliquam ab quaerat natus sapiente minus maxime unde pariatur fugit. Tempora dolores, placeat + fugiat dolore rerum quasi? +
    +
    + Subline Text Lorem ipsum dolor sit amet consectetur adipisicing elit. Enim minus id maxime quis vel + nostrum fugiat exercitationem, nesciunt delectus optio quae ad explicabo, iure nemo atque recusandae + laudantium earum accusantium! +
    +
    + Subline Text Lorem ipsum dolor sit amet consectetur adipisicing elit. Iure esse reiciendis porro hic + quos in animi incidunt mollitia aut, quam distinctio. Itaque architecto amet neque reiciendis, enim + officia vero numquam. +
    +
    +
  • +
  • + +
    +
    + Title Lorem ipsum dolor sit amet, consectetur adipisicing elit. Voluptas, deserunt. Consectetur, aliquid + tempore! Aliquam ab quaerat natus sapiente minus maxime unde pariatur fugit. Tempora dolores, placeat + fugiat dolore rerum quasi? +
    +
    + Subline Text Lorem ipsum dolor sit amet consectetur adipisicing elit. Enim minus id maxime quis vel + nostrum fugiat exercitationem, nesciunt delectus optio quae ad explicabo, iure nemo atque recusandae + laudantium earum accusantium! +
    +
    + Subline Text Lorem ipsum dolor sit amet consectetur adipisicing elit. Iure esse reiciendis porro hic + quos in animi incidunt mollitia aut, quam distinctio. Itaque architecto amet neque reiciendis, enim + officia vero numquam. +
  • diff --git a/libs/docs/core/user-menu/examples/user-menu-default-example.component.html b/libs/docs/core/user-menu/examples/user-menu-default-example.component.html index 5b7d4a65bbc..bd95e26f78e 100644 --- a/libs/docs/core/user-menu/examples/user-menu-default-example.component.html +++ b/libs/docs/core/user-menu/examples/user-menu-default-example.component.html @@ -24,6 +24,7 @@
    Lisa Miller
    lisa.miller@test.com
    User Experience Designer
    +
    Primary Employment
    @@ -36,18 +37,18 @@ [expandAriaLabel]="expanded ? 'Hide other accounts' : 'Show other accounts'" expandAriaLabelledBy="panel-expandable-title-1" > -
    Other Accounts
    +
    Accounts (3)
    • - +
      Lisa Miller
      lisa.miller@test.com
      @@ -55,7 +56,7 @@
      Other Accounts
    • - +
      John Doe
      john.doe@test.com
      @@ -63,7 +64,7 @@
      Other Accounts
    • - +
      Jane Doe
      jane.doe@test.com
      @@ -81,30 +82,20 @@
      Other Accounts
      text="Settings" (click)="actionPicked('Settings')" >
    • -
    • +
    • +
    • - +
    • -
    • +
    @@ -117,7 +108,7 @@
    Other Accounts
    fd-button label="Sign Out" fdType="transparent" - glyph="filter" + glyph="log" ariaLabel="Transparent Filter" > diff --git a/libs/docs/core/user-menu/examples/user-menu-mobile-example.component.html b/libs/docs/core/user-menu/examples/user-menu-mobile-example.component.html index 5041bde76d5..3d8313bc970 100644 --- a/libs/docs/core/user-menu/examples/user-menu-mobile-example.component.html +++ b/libs/docs/core/user-menu/examples/user-menu-mobile-example.component.html @@ -24,6 +24,7 @@
    Lisa Miller
    lisa.miller@test.com
    User Experience Designer
    +
    Primary Employment
    @@ -36,18 +37,18 @@ [expandAriaLabel]="expanded ? 'Hide other accounts' : 'Show other accounts'" expandAriaLabelledBy="panel-expandable-title-1" > -
    Other Accounts
    +
    Accounts (3)
    • - +
      Lisa Miller
      lisa.miller@test.com
      @@ -55,7 +56,7 @@
      Other Accounts
    • - +
      John Doe
      john.doe@test.com
      @@ -63,7 +64,7 @@
      Other Accounts
    • - +
      Jane Doe
      jane.doe@test.com
      @@ -81,30 +82,20 @@
      Other Accounts
      text="Settings" (click)="actionPicked('Settings')" >
    • -
    • +
    • +
    • - +
    • -
    • +
    @@ -117,7 +108,7 @@
    Other Accounts
    fd-button label="Sign Out" fdType="transparent" - glyph="filter" + glyph="log" ariaLabel="Transparent Filter" > diff --git a/libs/docs/core/user-menu/user-menu-header/user-menu-header.component.html b/libs/docs/core/user-menu/user-menu-header/user-menu-header.component.html index 5211050ae12..e70cb31ff10 100644 --- a/libs/docs/core/user-menu/user-menu-header/user-menu-header.component.html +++ b/libs/docs/core/user-menu/user-menu-header/user-menu-header.component.html @@ -78,8 +78,21 @@

    Directives

    • fd-user-menu-header
    • fd-user-menu-header-container
    • -
    • fd-user-menu-user-name
    • -
    • fd-user-menu-subline
    • +
    • + fd-user-menu-user-name +
        +
      • + truncate input (boolean) - whether the text should truncate with ellipsis after the + second line. +
      • +
      +
    • +
    • + fd-user-menu-subline +
        +
      • truncate input (boolean) - whether the text should truncate with ellipsis.
      • +
      +