Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(core): user settings #13041

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all 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 @@ -174,7 +174,7 @@ export class ContentDensityObserver extends BehaviorSubject<ContentDensityMode>
contentDensityDirective: this._contentDensityDirective ?? undefined,
contentDensityService: this._globalContentDensityService ?? undefined,
parentContentDensityService: this.config.restrictChildContentDensity
? this._parentContentDensityObserver?.asObservable() ?? undefined
? (this._parentContentDensityObserver?.asObservable() ?? undefined)
: undefined
})
.pipe(
Expand Down
5 changes: 3 additions & 2 deletions libs/core/datetime/fd-datetime-adapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -156,8 +156,9 @@ export class FdDatetimeAdapter extends DatetimeAdapter<FdDate> {

try {
const am = formatter.formatToParts(new Date(2020, 0, 1, 6)).find(({ type }) => type === 'dayPeriod')?.value;
const pm = formatter.formatToParts(new Date(2020, 0, 1, 16)).find(({ type }) => type === 'dayPeriod')
?.value;
const pm = formatter
.formatToParts(new Date(2020, 0, 1, 16))
.find(({ type }) => type === 'dayPeriod')?.value;

return am && pm ? [am, pm] : DEFAULT_PERIODS;
} catch {
Expand Down
3 changes: 3 additions & 0 deletions libs/core/dialog/base/dialog-config-base.class.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,9 @@ export class DialogConfigBase<T> implements DynamicComponentConfig {
/** Whether to completely disable dialog body paddings. */
disablePaddings?: boolean = false;

/** Whether the dialog is a Settings dialog. */
settings?: boolean;

/** Workaround for IE11, as `flex-grow: 1` on dialog body won't work when 'min-height' for dialog set
* There is another way to get dialog of wanted height by setting `min-height` for dialog body.
*/
Expand Down
3 changes: 2 additions & 1 deletion libs/core/dialog/dialog-body/dialog-body.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,10 +31,11 @@ import { DialogRef } from '../utils/dialog-ref.class';
templateUrl: 'dialog-body.component.html',
host: {
'[class.fd-dialog__body]': 'true',
'[class.fd-settings__dialog-body]': 'dialogConfig.settings',
'[class.fd-dialog__body--no-vertical-padding]': '!dialogConfig.verticalPadding',
'[class.fd-dialog__body--no-horizontal-padding]': '!dialogConfig.horizontalPadding',
'[style.min-height]': 'dialogConfig.bodyMinHeight',
'[style.padding]': 'dialogConfig.disablePaddings || disablePaddings() ? 0 : "1rem"'
'[style.padding]': 'dialogConfig.disablePaddings || disablePaddings() || dialogConfig.settings ? 0 : "1rem"'
},
providers: [
{
Expand Down
1 change: 1 addition & 0 deletions libs/core/dialog/dialog.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
[class.fd-dialog__content--draggable-grab]="dialogConfig.draggable && !isDragged"
[class.fd-dialog__content--draggable-grabbing]="dialogConfig.draggable && isDragged"
[class.fd-dialog__content-full-screen]="_fullScreen"
[class.fd-settings__dialog-content]="dialogConfig.settings"
[cdkDragDisabled]="!dialogConfig.draggable"
[fdkResizeDisabled]="!dialogConfig.resizable"
[attr.id]="dialogConfig.id"
Expand Down
1 change: 1 addition & 0 deletions libs/core/dialog/dialog.component.scss
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
@import 'fundamental-styles/dist/dialog.css';
@import 'fundamental-styles/dist/settings.css';

.fd-dialog {
z-index: 999;
Expand Down
1 change: 1 addition & 0 deletions libs/core/dialog/dialog.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,7 @@ export class DialogComponent
this.dialogConfig.hasBackdrop ? 'fd-dialog' : 'fd-dialog--no-backdrop',
this.dialogConfig.container !== 'body' || this.dialogConfig.position ? 'fd-dialog--targeted' : '',
this.showDialogWindow ? 'fd-dialog--active' : '',
this.dialogConfig.settings ? 'fd-settings' : '',
this._class,
this.dialogConfig.backdropClass ? this.dialogConfig.backdropClass : ''
];
Expand Down
4 changes: 3 additions & 1 deletion libs/core/fundamental-ngx.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ import { ScrollSpyModule } from '@fundamental-ngx/core/scroll-spy';
import { ScrollbarModule } from '@fundamental-ngx/core/scrollbar';
import { SegmentedButtonModule } from '@fundamental-ngx/core/segmented-button';
import { SelectModule } from '@fundamental-ngx/core/select';
import { SettingsModule } from '@fundamental-ngx/core/settings';
import { ShellbarModule } from '@fundamental-ngx/core/shellbar';
import { SideNavigationModule } from '@fundamental-ngx/core/side-navigation';
import { SkeletonModule } from '@fundamental-ngx/core/skeleton';
Expand Down Expand Up @@ -189,7 +190,8 @@ import { WizardModule } from '@fundamental-ngx/core/wizard';
ContentDensityModule,
SkeletonModule,
MultiComboboxModule,
ObjectAttributeModule
ObjectAttributeModule,
SettingsModule
]
})
export class FundamentalNgxCoreModule {}
1 change: 1 addition & 0 deletions libs/core/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ export * from '@fundamental-ngx/core/scroll-spy';
export * from '@fundamental-ngx/core/scrollbar';
export * from '@fundamental-ngx/core/segmented-button';
export * from '@fundamental-ngx/core/select';
export * from '@fundamental-ngx/core/settings';
export * from '@fundamental-ngx/core/shared';
export * from '@fundamental-ngx/core/shellbar';
export * from '@fundamental-ngx/core/side-navigation';
Expand Down
5 changes: 5 additions & 0 deletions libs/core/list/list-item/list-item.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,11 @@ import {
HostBinding,
HostListener,
inject,
input,
Input,
Output,
QueryList,
TemplateRef,
ViewEncapsulation
} from '@angular/core';

Expand Down Expand Up @@ -171,6 +173,9 @@ export class ListItemComponent<T = any> extends ListFocusItem<T> implements Afte
optional: true
});

/** Template ref for Settings list item */
settingsListTpl = input<TemplateRef<any>>();

/** @hidden */
private _role = 'listitem'; // default for li elements

Expand Down
14 changes: 12 additions & 2 deletions libs/core/list/list.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,9 @@ import {
Output,
QueryList,
ViewEncapsulation,
inject
booleanAttribute,
inject,
input
} from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import {
Expand Down Expand Up @@ -44,7 +46,9 @@ import { FD_LIST_COMPONENT, FD_LIST_UNREAD_INDICATOR } from './tokens';
selector: '[fd-list], [fdList]',
template: `<ng-content></ng-content>`,
host: {
class: 'fd-list'
class: 'fd-list',
'[class.fd-settings__list]': 'settingsList() || settingsListFooter()',
'[class.fd-settings__list--footer]': 'settingsListFooter()'
},
styleUrls: ['./list.component.scss', '../../cdk/utils/drag-and-drop/drag-and-drop.scss'],
encapsulation: ViewEncapsulation.None,
Expand Down Expand Up @@ -145,6 +149,12 @@ export class ListComponent implements ListComponentInterface, ListUnreadIndicato
return this.role || this._defaultRole;
}

/** Whether the list is used inside Settings Dialog */
settingsList = input(false, { transform: booleanAttribute });

/** Whether the list is used inside Settings Dialog Footer */
settingsListFooter = input(false, { transform: booleanAttribute });

/**
* @hidden
* Default role for lists
Expand Down
11 changes: 11 additions & 0 deletions libs/core/settings/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
export * from './settings-container/settings-container.component';
export * from './settings-content/settings-content.directive';
export * from './settings-detail-area/settings-detail-area.directive';
export * from './settings-dialog-body/settings-dialog-body.directive';
export * from './settings-dialog-content/settings-dialog-content.directive';
export * from './settings-header-button/settings-header-button.directive';
export * from './settings-header/settings-header.directive';
export * from './settings-list-area/settings-list-area.directive';
export * from './settings-list-container/settings-list-container.directive';
export * from './settings.component';
export * from './settings.module';
6 changes: 6 additions & 0 deletions libs/core/settings/ng-package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"$schema": "../../../node_modules/ng-packagr/ng-package.schema.json",
"lib": {
"entryFile": "./index.ts"
}
}
10 changes: 10 additions & 0 deletions libs/core/settings/project.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"name": "core-settings",
"$schema": "../../../node_modules/nx/schemas/project-schema.json",
"projectType": "library",
"sourceRoot": "libs/core/settings",
"prefix": "fd",
"tags": ["type:lib", "scope:fd"],
"// targets": "to see all targets run: nx show project core-settings --web",
"targets": {}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
import { Component, ElementRef, ViewChild } from '@angular/core';
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import { SettingsContainerComponent } from '@fundamental-ngx/core/settings';
import { ListItemComponent } from '@fundamental-ngx/core/list';
import { ButtonComponent } from '@fundamental-ngx/core/button';
import { SettingsHeaderButtonDirective } from '@fundamental-ngx/core/settings';

@Component({
template: `
<fd-settings-container #componentElement>
<div fd-settings-list-area>
<div fd-settings-list-container>
<ul fd-list settingsList>
<li fd-list-item #listItem></li>
</ul>
</div>
</div>
<div fd-settings-detail-area>
<fd-button fd-settings-header-button>Header Button</fd-button>
Detail Area
</div>
</fd-settings-container>
`,
standalone: true,
imports: [SettingsContainerComponent, ListItemComponent, ButtonComponent, SettingsHeaderButtonDirective]
})
class TestComponent {
@ViewChild('componentElement', { static: true }) ref!: ElementRef;
@ViewChild(SettingsContainerComponent, { static: true }) settingsContainer!: SettingsContainerComponent;
@ViewChild('listItem', { static: true }) listItem!: ListItemComponent;
}

describe('SettingsContainerComponent', () => {
let component: TestComponent;
let fixture: ComponentFixture<TestComponent>;
let settingsContainer: SettingsContainerComponent;

beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
imports: [TestComponent]
}).compileComponents();
}));

beforeEach(() => {
fixture = TestBed.createComponent(TestComponent);
component = fixture.componentInstance;
settingsContainer = component.settingsContainer;
fixture.detectChanges();
});

it('should create the component', () => {
expect(component).toBeTruthy();
expect(settingsContainer).toBeTruthy();
});

it('should apply the correct default class', () => {
expect(component.ref.nativeElement.classList.contains('fd-settings__container')).toBeTrue();
});

it('should apply view mode classes based on screen width', () => {
spyOnProperty(settingsContainer, 'screenWidth', 'get').and.returnValue(500);
settingsContainer.onWindowResize();
fixture.detectChanges();
expect(component.ref.nativeElement.classList.contains('fd-settings__container--sm')).toBeTrue();

spyOnProperty(settingsContainer, 'screenWidth', 'get').and.returnValue(800);
settingsContainer.onWindowResize();
fixture.detectChanges();
expect(component.ref.nativeElement.classList.contains('fd-settings__container--md')).toBeTrue();

spyOnProperty(settingsContainer, 'screenWidth', 'get').and.returnValue(1400);
settingsContainer.onWindowResize();
fixture.detectChanges();
expect(component.ref.nativeElement.classList.contains('fd-settings__container--lg')).toBeTrue();
});

it('should toggle list and detail area visibility based on view mode', () => {
spyOnProperty(settingsContainer, 'screenWidth', 'get').and.returnValue(500);
settingsContainer.onWindowResize();
fixture.detectChanges();
expect(settingsContainer.showListArea()).toBeTrue();
expect(settingsContainer.showDetailArea()).toBeFalse();

spyOnProperty(settingsContainer, 'screenWidth', 'get').and.returnValue(1400);
settingsContainer.onWindowResize();
fixture.detectChanges();
expect(settingsContainer.showListArea()).toBeTrue();
expect(settingsContainer.showDetailArea()).toBeTrue();
});

it('should update active list item on click', () => {
const listItem = component.listItem;
listItem.selected = false;
fixture.detectChanges();

listItem.elementRef.nativeElement.click();
fixture.detectChanges();

expect(settingsContainer.activeListItem()).toBe(listItem);
expect(listItem.selected).toBeTrue();
});

it('should hide list area and show detail area when list item is clicked in non-lg view mode', () => {
spyOnProperty(settingsContainer, 'screenWidth', 'get').and.returnValue(800);
settingsContainer.onWindowResize();
fixture.detectChanges();

component.listItem.elementRef.nativeElement.click();
fixture.detectChanges();

expect(settingsContainer.showListArea()).toBeFalse();
expect(settingsContainer.showDetailArea()).toBeTrue();
});

it('should toggle visibility of header button based on view mode', () => {
const buttonElement = fixture.nativeElement.querySelector('[fd-settings-header-button]');
expect(buttonElement).toBeTruthy();

spyOnProperty(settingsContainer, 'screenWidth', 'get').and.returnValue(1400);
settingsContainer.onWindowResize();
fixture.detectChanges();
expect(buttonElement.style.display).toBe('none');

spyOnProperty(settingsContainer, 'screenWidth', 'get').and.returnValue(500);
settingsContainer.onWindowResize();
fixture.detectChanges();
expect(buttonElement.style.display).toBe('block');
});

it('should show list area and hide detail area when header button is clicked in non-lg mode', () => {
const buttonElement = fixture.nativeElement.querySelector('[fd-settings-header-button]');
spyOnProperty(settingsContainer, 'screenWidth', 'get').and.returnValue(500);
settingsContainer.onWindowResize();
fixture.detectChanges();

buttonElement.click();
fixture.detectChanges();

expect(settingsContainer.showListArea()).toBeTrue();
expect(settingsContainer.showDetailArea()).toBeFalse();
});

it('should clean up event listeners on destroy', () => {
spyOn(settingsContainer['_eventUnlisteners'], 'forEach');
settingsContainer.ngOnDestroy();
expect(settingsContainer['_eventUnlisteners'].forEach).toHaveBeenCalled();
expect(settingsContainer['_eventUnlisteners'].length).toBe(0);
});
});
Loading
Loading