diff --git a/projects/dxc-ngx-cdk/src/lib/dxc-badge/dxc-badge.component.html b/projects/dxc-ngx-cdk/src/lib/dxc-badge/dxc-badge.component.html new file mode 100644 index 000000000..d9c39e65d --- /dev/null +++ b/projects/dxc-ngx-cdk/src/lib/dxc-badge/dxc-badge.component.html @@ -0,0 +1 @@ +{{notificationText}} \ No newline at end of file diff --git a/projects/dxc-ngx-cdk/src/lib/dxc-badge/dxc-badge.component.ts b/projects/dxc-ngx-cdk/src/lib/dxc-badge/dxc-badge.component.ts new file mode 100644 index 000000000..9e2fd2a23 --- /dev/null +++ b/projects/dxc-ngx-cdk/src/lib/dxc-badge/dxc-badge.component.ts @@ -0,0 +1,77 @@ +import { + Component, + OnInit, + Input, + SimpleChanges, + HostBinding, +} from "@angular/core"; +import { BehaviorSubject } from "rxjs"; +import { css } from "emotion"; +import { CssUtils } from "../utils"; +@Component({ + selector: "dxc-badge", + templateUrl: "./dxc-badge.component.html", + providers: [CssUtils], +}) +export class DxcBadgeComponent implements OnInit { + @HostBinding("class") className; + @Input() + notificationText: any; + + defaultInputs = new BehaviorSubject({}); + + ngOnChanges(changes: SimpleChanges): void { + if(this.notificationText > 99) { + this.notificationText = "+99"; + } + const inputs = Object.keys(changes).reduce((result, item) => { + result[item] = changes[item].currentValue; + return result; + }, {}); + this.defaultInputs.next({ ...this.defaultInputs.getValue(), ...inputs }); + this.className = `${this.getDynamicStyle(this.defaultInputs.getValue())}`; + } + + constructor(private utils: CssUtils) {} + + ngOnInit() { + this.className = `${this.getDynamicStyle(this.defaultInputs.getValue())}`; + } + + getDynamicStyle(inputs) { + return css` + display: flex; + padding-bottom: 1px; + padding-right: 1px; + flex-wrap: wrap; + box-sizing: border-box; + align-items: center; + line-height: 1; + align-content: center; + flex-direction: row; + justify-content: center; + background-color: var(--tabs-badgeBackgroundColor); + font-family: var(--tabs-badgeFontFamily); + font-size: var(--tabs-badgeFontSize); + font-style: var(--tabs-badgeFontStyle); + font-weight: var(--tabs-badgeFontWeight); + color: var(--tabs-badgeFontColor); + letter-spacing: var("--tabs-badgeLetterSpacing); + width: ${ + !this.notificationText + ? "var(--tabs-badgeWidth)" + : "var(--tabs-badgeWidthWithNotificationNumber)" + }; + height: ${ + !this.notificationText + ? "var(--tabs-badgeHeight)" + : "var(--tabs-badgeHeightWithNotificationNumber)" + }; + border-radius: ${ + !this.notificationText + ? "var(--tabs-badgeRadius)" + : "var(--tabs-badgeRadiusWithNotificationNumber)" + }; + `; + } +} diff --git a/projects/dxc-ngx-cdk/src/lib/dxc-badge/dxc-badge.module.ts b/projects/dxc-ngx-cdk/src/lib/dxc-badge/dxc-badge.module.ts new file mode 100644 index 000000000..29117fc56 --- /dev/null +++ b/projects/dxc-ngx-cdk/src/lib/dxc-badge/dxc-badge.module.ts @@ -0,0 +1,10 @@ +import { NgModule } from "@angular/core"; +import { DxcBadgeComponent } from "./dxc-badge.component"; +import { CommonModule } from "@angular/common"; + +@NgModule({ + declarations: [DxcBadgeComponent], + imports: [CommonModule], + exports: [DxcBadgeComponent], +}) +export class DxcBadgeModule {} diff --git a/projects/dxc-ngx-cdk/src/lib/dxc-card/dxc-card.component.html b/projects/dxc-ngx-cdk/src/lib/dxc-card/dxc-card.component.html index 53396fb98..26fde9d7c 100644 --- a/projects/dxc-ngx-cdk/src/lib/dxc-card/dxc-card.component.html +++ b/projects/dxc-ngx-cdk/src/lib/dxc-card/dxc-card.component.html @@ -1,5 +1,9 @@ - + -
+
+ + diff --git a/projects/dxc-ngx-cdk/src/lib/dxc-card/dxc-card.component.spec.ts b/projects/dxc-ngx-cdk/src/lib/dxc-card/dxc-card.component.spec.ts index 9abcaa063..52d3f2497 100644 --- a/projects/dxc-ngx-cdk/src/lib/dxc-card/dxc-card.component.spec.ts +++ b/projects/dxc-ngx-cdk/src/lib/dxc-card/dxc-card.component.spec.ts @@ -1,30 +1,46 @@ import { render, fireEvent } from "@testing-library/angular"; import { DxcCardComponent } from "./dxc-card.component"; import { MatCardModule } from "@angular/material/card"; +import { DxcBoxModule } from "../dxc-box/dxc-box.module"; +import { TestBed } from "@angular/core/testing"; +import { + BrowserDynamicTestingModule, + platformBrowserDynamicTesting, +} from "@angular/platform-browser-dynamic/testing"; +import { screen } from "@testing-library/dom"; +import { BackgroundProviderModule } from "../background-provider/background-provider.module"; + +TestBed.initTestEnvironment( + BrowserDynamicTestingModule, + platformBrowserDynamicTesting() +); describe("DxcCardComponent tests", () => { test("should render dxc-card", async () => { const projection = "Content inside the ng-content!"; - const dxcCard = await render(DxcCardComponent, { - template: `${projection}`, + await render(`${projection}`, { + imports: [BackgroundProviderModule, DxcBoxModule], componentProperties: {}, - imports: [MatCardModule], + declarations: [DxcCardComponent], }); - expect(dxcCard.getByText(projection)); + expect(screen.getByText(projection)); }); test("dxc-card onClick", async () => { const projection = "Content inside the ng-content!"; const onClickFunction = jest.fn(); - const dxcCard = await render(DxcCardComponent, { - template: `${projection}`, - componentProperties: { onClickFunction }, - imports: [MatCardModule], - }); + await render( + `${projection}`, + { + imports: [BackgroundProviderModule, DxcBoxModule], + componentProperties: { onClickFunction }, + declarations: [DxcCardComponent], + } + ); - expect(dxcCard.getByText(projection)); - fireEvent.click(dxcCard.getByText(projection)); + expect(screen.getByText(projection)); + fireEvent.click(screen.getByText(projection)); expect(onClickFunction).toHaveBeenCalled(); }); }); diff --git a/projects/dxc-ngx-cdk/src/lib/dxc-card/dxc-card.component.ts b/projects/dxc-ngx-cdk/src/lib/dxc-card/dxc-card.component.ts index 7b61d40be..1a39abd64 100644 --- a/projects/dxc-ngx-cdk/src/lib/dxc-card/dxc-card.component.ts +++ b/projects/dxc-ngx-cdk/src/lib/dxc-card/dxc-card.component.ts @@ -9,8 +9,7 @@ import { EventEmitter, SimpleChanges, ChangeDetectorRef, - Inject, - Optional + Optional, } from "@angular/core"; import { css } from "emotion"; import { BehaviorSubject } from "rxjs"; @@ -20,6 +19,7 @@ import { coerceNumberProperty, } from "@angular/cdk/coercion"; import { BackgroundProviderService } from "../background-provider/service/background-provider.service"; +import { Space, Spacing, CardProperties } from "./dxc-card.types" @Component({ selector: "dxc-card", @@ -28,9 +28,52 @@ import { BackgroundProviderService } from "../background-provider/service/backgr providers: [CssUtils], }) export class DxcCardComponent implements OnInit { + /** + * URL of the image that will be placed in the card component. + */ @Input() imageSrc: string; - @Input() imagePosition: string; - @Input() imagePadding: any; + + /** + * Color of the image background. + */ + @Input() imageBgColor: string = "black"; + + /** + * Size of the padding to be applied to the image section of the component + * ('xxsmall' | 'xsmall' | 'small' | 'medium' | 'large' | 'xlarge' | 'xxlarge'). + * You can pass an object with 'top', 'bottom', 'left' and 'right' properties + * in order to specify different padding sizes. + */ + @Input() imagePadding: Space | Spacing; + + /** + * Whether the image should appear in relation to the content. + */ + @Input() imagePosition: "after" | "before" = "before"; + + /** + * Size of the padding to be applied to the content section of the component + * ('xxsmall' | 'xsmall' | 'small' | 'medium' | 'large' | 'xlarge' | 'xxlarge'). + * You can pass an object with 'top', 'bottom', 'left' and 'right' properties + * in order to specify different padding sizes. + */ + @Input() contentPadding: Space | Spacing; + + /** + * If defined, the card will be displayed as an anchor, using this prop as "href". + * Component will show some visual feedback on hover. + */ + @Input() linkHref: string; + + /** + * This event will emit when the user clicks the card. Component will show some + * visual feedback on hover. + */ + @Output() onClick: EventEmitter = new EventEmitter(); + + /** + * Whether the image must cover the whole image area of the card. + */ @Input() get imageCover(): boolean { return this._imageCover; @@ -39,9 +82,18 @@ export class DxcCardComponent implements OnInit { this._imageCover = coerceBooleanProperty(value); } private _imageCover = false; - @Input() imageBgColor: string; - @Input() margin: any; - @Input() linkHref: string; + + /** + * Size of the margin to be applied to the component + * ('xxsmall' | 'xsmall' | 'small' | 'medium' | 'large' | 'xlarge' | 'xxlarge'). + * You can pass an object with 'top', 'bottom', 'left' and 'right' properties in + * order to specify different margin sizes. + */ + @Input() margin: Space | Spacing; + + /** + * Value of the tabindex given when there is an href. + */ @Input() get tabIndexValue(): number { return this._tabIndexValue; @@ -49,23 +101,30 @@ export class DxcCardComponent implements OnInit { set tabIndexValue(value: number) { this._tabIndexValue = coerceNumberProperty(value); } - private _tabIndexValue; + private _tabIndexValue = 0; - @Output() onClick = new EventEmitter(); + /** + * Whether the card must be outlined. + */ + @Input() outlined: boolean = true; + + private isHovered: boolean; @HostBinding("class") className; @ViewChild("content", { static: false }) content: ElementRef; - defaultInputs = new BehaviorSubject({ + defaultInputs = new BehaviorSubject({ imageSrc: null, - imagePosition: "before", + imageBgColor: "black", imagePadding: null, + imagePosition: "before", + contentPadding: null, + linkHref: null, imageCover: false, - imageBgColor: "black", margin: null, - linkHref: null, tabIndexValue: 0, + outlined: true, }); public ngOnChanges(changes: SimpleChanges): void { @@ -80,15 +139,14 @@ export class DxcCardComponent implements OnInit { this.className = `${this.getDynamicStyle(this.defaultInputs.getValue())}`; } - constructor(private utils: CssUtils, private cdRef: ChangeDetectorRef, - @Optional() @Inject("bgService") public bgProviderService: BackgroundProviderService) { - } + constructor( + private utils: CssUtils, + private cdRef: ChangeDetectorRef, + @Optional() public bgProviderService?: BackgroundProviderService + ) {} ngOnInit() { this.className = `${this.getDynamicStyle(this.defaultInputs.getValue())}`; - this.bgProviderService?.$changeColor?.subscribe(resp => { - console.log(resp); - }); } ngAfterContentChecked() { @@ -110,20 +168,30 @@ export class DxcCardComponent implements OnInit { applyTheme(href, outlined) { return css` - mat-card { - background-color: var(--card-backgroundColor); - color: black; - ${!outlined ? this.utils.getBoxShadow("1") : this.utils.getBoxShadow(0)} - } + mat-card { + ${this.utils.getBoxShadow(0, true)} + } mat-card:hover { - ${!outlined - ? this.utils.getBoxShadow(this.getShadowDepthOnHover(href)) - : this.utils.getBoxShadow("1")} + ${this.utils.getBoxShadow(0, true)} } `; } + changeIsHovered(isHovered: boolean) { + this.isHovered = isHovered; + } + + getShadowDepth() { + return !this.defaultInputs.value.outlined + ? "0" + : this.isHovered && + this.onClick.observers.length > 0 && + this.linkHref !== "" + ? "2" + : "1"; + } + getCursor(href) { if (this.onClick.observers.length > 0 || href) { return css` @@ -145,19 +213,20 @@ export class DxcCardComponent implements OnInit { getDynamicStyle(inputs) { return css` display: inline-flex; + ${this.utils.getMargins(inputs.margin)} + ${this.getCursor(inputs.linkHref)} + width: var(--card-width); + height: var(--card-height); + mat-card { - ${this.utils.getMargins(inputs.margin)} - ${this.getCursor(inputs.linkHref)} - font-family: var(--fontFamily); font-size: 14px; display: inline-flex; - width: 400px; - height: 220px; padding: 0px; ${this.tabIndexValue === -1 ? "outline:none;" : ""} .content { overflow: hidden; width: 260px; + ${this.utils.getPaddings(inputs.contentPadding)} } img, svg { @@ -223,7 +292,6 @@ export class DxcCardComponent implements OnInit { border-bottom-right-radius: 4px; } } - ${this.applyTheme(inputs.linkHref, inputs.outlined)} `; } diff --git a/projects/dxc-ngx-cdk/src/lib/dxc-card/dxc-card.module.ts b/projects/dxc-ngx-cdk/src/lib/dxc-card/dxc-card.module.ts index 5abae8f04..076d72ee6 100644 --- a/projects/dxc-ngx-cdk/src/lib/dxc-card/dxc-card.module.ts +++ b/projects/dxc-ngx-cdk/src/lib/dxc-card/dxc-card.module.ts @@ -3,10 +3,11 @@ import { MatCardModule } from "@angular/material/card"; import { DxcCardComponent } from "./dxc-card.component"; import { CommonModule } from "@angular/common"; import { BackgroundProviderModule } from "../background-provider/background-provider.module"; +import { DxcBoxModule } from '../dxc-box/dxc-box.module'; @NgModule({ declarations: [DxcCardComponent], - imports: [CommonModule, MatCardModule, BackgroundProviderModule], + imports: [CommonModule, MatCardModule, DxcBoxModule, BackgroundProviderModule], exports: [DxcCardComponent] }) export class DxcCardModule {} diff --git a/projects/dxc-ngx-cdk/src/lib/dxc-card/dxc-card.types.ts b/projects/dxc-ngx-cdk/src/lib/dxc-card/dxc-card.types.ts new file mode 100644 index 000000000..e9a785ae2 --- /dev/null +++ b/projects/dxc-ngx-cdk/src/lib/dxc-card/dxc-card.types.ts @@ -0,0 +1,28 @@ +export type Space = + | "xxsmall" + | "xsmall" + | "small" + | "medium" + | "large" + | "xlarge" + | "xxlarge"; + +export type Spacing = { + top?: Space; + bottom?: Space; + left?: Space; + right?: Space; +}; + +export interface CardProperties { + imageSrc: string; + imageBgColor: string; + imagePadding: Space | Spacing; + imagePosition: "after" | "before"; + contentPadding: Space | Spacing; + linkHref: string; + imageCover: boolean; + margin: Space | Spacing; + tabIndexValue: number; + outlined: boolean; +} \ No newline at end of file diff --git a/projects/dxc-ngx-cdk/src/lib/dxc-tabs/dxc-tab/dxc-tab.component.html b/projects/dxc-ngx-cdk/src/lib/dxc-tabs/dxc-tab/dxc-tab.component.html index 611060c2b..54398c26f 100644 --- a/projects/dxc-ngx-cdk/src/lib/dxc-tabs/dxc-tab/dxc-tab.component.html +++ b/projects/dxc-ngx-cdk/src/lib/dxc-tabs/dxc-tab/dxc-tab.component.html @@ -1,12 +1,16 @@ -
+
-
+
{{ label }}
+
- \ No newline at end of file + diff --git a/projects/dxc-ngx-cdk/src/lib/dxc-tabs/dxc-tab/dxc-tab.component.ts b/projects/dxc-ngx-cdk/src/lib/dxc-tabs/dxc-tab/dxc-tab.component.ts index bc252a228..4e618f768 100644 --- a/projects/dxc-ngx-cdk/src/lib/dxc-tabs/dxc-tab/dxc-tab.component.ts +++ b/projects/dxc-ngx-cdk/src/lib/dxc-tabs/dxc-tab/dxc-tab.component.ts @@ -17,13 +17,43 @@ import { TabService } from "../services/tab.service"; templateUrl: "./dxc-tab.component.html", }) export class DxcTabComponent implements OnChanges { - //Default values + /** + * Text to be placed within the tab. + */ @Input() label: string; + + /** + * @deprecated The path of an icon to be placed within the tab. + */ @Input() iconSrc: string; + + /** + * Whether the tab is disabled or not. + */ @Input() disabled: boolean = false; + + /** + * It can have boolean type or number type. + * If the value is 'true', an empty badge will appear. If it is 'false', + * no badge will appear. + * If a number is put it will be shown as the label of the notification + * in the tab, taking into account that if that number is greater than 99, + * it will appear as '+99' in the badge. + */ + @Input() notificationNumber: boolean | number; + + /** + * This event will emit when the user clicks on a tab. The index + * of the clicked tab will be passed as a parameter. + */ + @Output() onTabClick: EventEmitter = new EventEmitter(); + + /** + * This event will emit when the user is on hover on a tab. + */ + @Output() onTabHover: EventEmitter = new EventEmitter(); + @Input() id: number; - @Output() onTabClick = new EventEmitter(); - @Output() onTabHover = new EventEmitter(); showDotIndicator: boolean = false; labelClass: string; @@ -37,6 +67,8 @@ export class DxcTabComponent implements OnChanges { iconPosition: string; + notificationValue: any; + constructor(private cdRef: ChangeDetectorRef, private service: TabService) { this.service.iconPosition.subscribe((value) => { if (value) { @@ -46,6 +78,13 @@ export class DxcTabComponent implements OnChanges { }); } + public ngOnInit(): void { + this.notificationValue = + typeof this.notificationNumber === "boolean" + ? "" + : this.notificationNumber; + } + public ngOnChanges(): void { this.getLabelClass(); if (this.matTab) { @@ -60,12 +99,16 @@ export class DxcTabComponent implements OnChanges { this.tabIcon = true; } this.getLabelClass(); - this.cdRef.detectChanges(); this.matTab.disabled = this.disabled; + this.cdRef.detectChanges(); } public onClickHandler(): void { - this.onTabClick.emit(this.id); + if (!this.matTab.disabled) { + this.onTabClick.emit(this.id); + } else { + this.matTab.isActive = false; + } } public onHoverHandler(): void { diff --git a/projects/dxc-ngx-cdk/src/lib/dxc-tabs/dxc-tabs.component.html b/projects/dxc-ngx-cdk/src/lib/dxc-tabs/dxc-tabs.component.html index b49e34cf1..cd8d880ec 100644 --- a/projects/dxc-ngx-cdk/src/lib/dxc-tabs/dxc-tabs.component.html +++ b/projects/dxc-ngx-cdk/src/lib/dxc-tabs/dxc-tabs.component.html @@ -1,7 +1,7 @@ diff --git a/projects/dxc-ngx-cdk/src/lib/dxc-tabs/dxc-tabs.component.scss b/projects/dxc-ngx-cdk/src/lib/dxc-tabs/dxc-tabs.component.scss index 86f87529e..a37095a27 100644 --- a/projects/dxc-ngx-cdk/src/lib/dxc-tabs/dxc-tabs.component.scss +++ b/projects/dxc-ngx-cdk/src/lib/dxc-tabs/dxc-tabs.component.scss @@ -2,7 +2,7 @@ .filled-tabs { ::ng-deep { .mat-ink-bar { - background-color: var(--tabs-selectedUnderlinedColor); + background-color: var(--tabs-selectedUnderlineColor); } } } diff --git a/projects/dxc-ngx-cdk/src/lib/dxc-tabs/dxc-tabs.component.spec.ts b/projects/dxc-ngx-cdk/src/lib/dxc-tabs/dxc-tabs.component.spec.ts index ea6623756..135e01dfc 100644 --- a/projects/dxc-ngx-cdk/src/lib/dxc-tabs/dxc-tabs.component.spec.ts +++ b/projects/dxc-ngx-cdk/src/lib/dxc-tabs/dxc-tabs.component.spec.ts @@ -1,71 +1,146 @@ import { render, fireEvent } from "@testing-library/angular"; import { screen } from "@testing-library/dom"; -import { DxcTabsComponent } from "./dxc-tabs.component"; -import { DxcTabsModule } from './dxc-tabs.module'; +import { DxcTabsModule } from "./dxc-tabs.module"; +import { + BrowserDynamicTestingModule, + platformBrowserDynamicTesting, +} from "@angular/platform-browser-dynamic/testing"; +import { TestBed } from "@angular/core/testing"; + +TestBed.initTestEnvironment( + BrowserDynamicTestingModule, + platformBrowserDynamicTesting() +); describe("DxcTabs tests", () => { test("should render dxc-tabs", async () => { - const tabs = await render(DxcTabsComponent, { - template: ` - - - - `, - componentProperties: { }, - imports: [DxcTabsModule], - excludeComponentDeclaration: true - }); + const tabs = await render( + ` + + + + `, + { + componentProperties: {}, + imports: [DxcTabsModule], + excludeComponentDeclaration: true, + } + ); tabs.detectChanges(); expect(tabs.getByText("Tab1")).toBeTruthy(); expect(tabs.getByText("Tab2")).toBeTruthy(); expect(tabs.getByText("Tab3")).toBeTruthy(); - }); + }); + + test("should render dxc-tabs with default value", async () => { + const clickFunction = jest.fn(); + const tabs = await render( + ` + + + + `, + { + componentProperties: { clickFunction }, + imports: [DxcTabsModule], + excludeComponentDeclaration: true, + } + ); + tabs.detectChanges(); + const arrTabs = screen.getAllByRole("tab"); + expect(arrTabs[0].getAttribute("aria-selected")).toBe("false"); + expect(arrTabs[1].getAttribute("aria-selected")).toBe("false"); + expect(arrTabs[2].getAttribute("aria-selected")).toBe("true"); + expect(tabs.getByText("Tab1")).toBeTruthy(); + expect(tabs.getByText("Tab2")).toBeTruthy(); + expect(tabs.getByText("Tab3")).toBeTruthy(); + + const tab1 = screen.getByText("Tab1"); + fireEvent.click(tab1); + tabs.detectChanges(); + expect(clickFunction).toHaveBeenCalledWith(0); + expect(arrTabs[0].getAttribute("aria-selected")).toBe("true"); + expect(arrTabs[1].getAttribute("aria-selected")).toBe("false"); + expect(arrTabs[2].getAttribute("aria-selected")).toBe("false"); + }); - test("should render uncontrolled tabs", async () => { - const clickFunction = jest.fn(); - const tabs = await render(DxcTabsComponent, { - template: ` - - - - `, + test("should render dxc-badge", async () => { + const tabs = await render( + ` + + + + `, + { + componentProperties: {}, + imports: [DxcTabsModule], + excludeComponentDeclaration: true, + } + ); + tabs.detectChanges(); + expect(tabs.getByText("90")).toBeTruthy(); + expect(tabs.getByText("+99")).toBeTruthy(); + }); + + test("should render uncontrolled tabs", async () => { + const clickFunction = jest.fn(); + const tabs = await render( + ` + + + + `, + { componentProperties: { clickFunction }, imports: [DxcTabsModule], - excludeComponentDeclaration: true - }); - tabs.detectChanges(); - const tab1 = screen.getByText("Tab1"); - const tab2 = screen.getByText("Tab2"); - fireEvent.click(tab2); - tabs.detectChanges(); - expect(clickFunction).toHaveBeenCalledWith(1); - tabs.detectChanges(); - fireEvent.click(tab1); - tabs.detectChanges(); - expect(clickFunction).toHaveBeenCalledWith(0); - }); + excludeComponentDeclaration: true, + } + ); + tabs.detectChanges(); + const tab1 = screen.getByText("Tab1"); + const tab2 = screen.getByText("Tab2"); + fireEvent.click(tab2); + tabs.detectChanges(); + expect(clickFunction).toHaveBeenCalledWith(1); + tabs.detectChanges(); + fireEvent.click(tab1); + tabs.detectChanges(); + expect(clickFunction).toHaveBeenCalledWith(0); + }); - test("should render controlled tabs", async () => { - const clickFunction = jest.fn(); - const tabs = await render(DxcTabsComponent, { - template: ` - - - - `, + test("should render controlled tabs", async () => { + const clickFunction = jest.fn(); + const tabs = await render( + ` + + + + `, + { componentProperties: { clickFunction }, imports: [DxcTabsModule], - excludeComponentDeclaration: true - }); - tabs.detectChanges(); - const tab2 = screen.getByText("Tab2"); - const tab3 = screen.getByText("Tab3"); - fireEvent.click(tab2); - tabs.detectChanges(); - expect(clickFunction).toHaveBeenCalledWith(1); - tabs.detectChanges(); - fireEvent.click(tab3); - tabs.detectChanges(); - expect(clickFunction).toHaveBeenCalledWith(2); - }); -}); \ No newline at end of file + excludeComponentDeclaration: true, + } + ); + tabs.detectChanges(); + const arrTabs = screen.getAllByRole("tab"); + expect(arrTabs[0].getAttribute("aria-selected")).toBe("true"); + expect(arrTabs[1].getAttribute("aria-selected")).toBe("false"); + expect(arrTabs[2].getAttribute("aria-selected")).toBe("false"); + const tab2 = screen.getByText("Tab2"); + const tab3 = screen.getByText("Tab3"); + fireEvent.click(tab2); + tabs.detectChanges(); + expect(clickFunction).toHaveBeenCalledWith(1); + expect(arrTabs[0].getAttribute("aria-selected")).toBe("false"); + expect(arrTabs[1].getAttribute("aria-selected")).toBe("true"); + expect(arrTabs[2].getAttribute("aria-selected")).toBe("false"); + tabs.detectChanges(); + fireEvent.click(tab3); + tabs.detectChanges(); + expect(clickFunction).toHaveBeenCalledWith(2); + expect(arrTabs[0].getAttribute("aria-selected")).toBe("false"); + expect(arrTabs[1].getAttribute("aria-selected")).toBe("false"); + expect(arrTabs[2].getAttribute("aria-selected")).toBe("true"); + }); +}); diff --git a/projects/dxc-ngx-cdk/src/lib/dxc-tabs/dxc-tabs.component.ts b/projects/dxc-ngx-cdk/src/lib/dxc-tabs/dxc-tabs.component.ts index a563ae318..b042676ce 100644 --- a/projects/dxc-ngx-cdk/src/lib/dxc-tabs/dxc-tabs.component.ts +++ b/projects/dxc-ngx-cdk/src/lib/dxc-tabs/dxc-tabs.component.ts @@ -21,6 +21,7 @@ import { MAT_RIPPLE_GLOBAL_OPTIONS, RippleGlobalOptions, } from "@angular/material/core"; +import { TabsProperties, Spacing, Space } from "./dxc-tabs.types"; const globalRippleConfig: RippleGlobalOptions = { animation: { @@ -28,6 +29,7 @@ const globalRippleConfig: RippleGlobalOptions = { exitDuration: 0, }, }; + @Component({ selector: "dxc-tabs", templateUrl: "./dxc-tabs.component.html", @@ -39,13 +41,9 @@ const globalRippleConfig: RippleGlobalOptions = { ], }) export class DxcTabsComponent implements OnChanges { - @HostBinding("class") className; - @HostBinding("class.label-icons") allTabWithLabelAndIcon: boolean = false; - - //Default values - @Input() margin: any; - @Input() iconPosition: string; - + /** + * The index of the active tab. + */ @Input() get activeTabIndex(): number { return this._activeTabIndex; @@ -53,7 +51,36 @@ export class DxcTabsComponent implements OnChanges { set activeTabIndex(value: number) { this._activeTabIndex = coerceNumberProperty(value); } - private _activeTabIndex; + private _activeTabIndex = 0; + + /** + * Initially active tab, only when it is uncontrolled + */ + @Input() + get defaultActiveTabIndex(): number { + return this._defaultActiveTabIndex; + } + set defaultActiveTabIndex(value: number) { + this._defaultActiveTabIndex = coerceNumberProperty(value); + } + private _defaultActiveTabIndex = 0; + + /** + * Position of icons in tabs. + */ + @Input() iconPosition: "top" | "left" = "left"; + + /** + * Size of the margin to be applied to the component + * ('xxsmall' | 'xsmall' | 'small' | 'medium' | 'large' | 'xlarge' | 'xxlarge'). + * You can pass an object with 'top', 'bottom', 'left' and 'right' properties + * in order to specify different margin sizes. + */ + @Input() margin: Space | Spacing; + + @HostBinding("class") className; + @HostBinding("class.label-icons") allTabWithLabelAndIcon: boolean = false; + renderedActiveTabIndex: number; @ViewChild(MatRipple) ripple: MatRipple; @@ -64,9 +91,10 @@ export class DxcTabsComponent implements OnChanges { @ContentChildren(DxcTabComponent) protected tabs: QueryList; - defaultInputs = new BehaviorSubject({ + defaultInputs = new BehaviorSubject({ + activeTabIndex: 0, + iconPosition: "left", margin: null, - iconPosition: null, }); constructor( @@ -82,7 +110,6 @@ export class DxcTabsComponent implements OnChanges { if (this.tabs && this.tabs.length > 0) { this.generateTabs(); } - const inputs = Object.keys(changes).reduce((result, item) => { result[item] = changes[item].currentValue; return result; @@ -93,6 +120,9 @@ export class DxcTabsComponent implements OnChanges { ngOnInit() { this.service.iconPosition.next(this.iconPosition || "left"); + this.activeTabIndex = this.defaultActiveTabIndex + ? this.defaultActiveTabIndex + : this.activeTabIndex; this.renderedActiveTabIndex = this.activeTabIndex; this.className = `${this.getDynamicStyle(this.defaultInputs.getValue())}`; } @@ -114,6 +144,29 @@ export class DxcTabsComponent implements OnChanges { this.cdRef.detectChanges(); this.setEventListeners(); this.cdRef.detectChanges(); + + this.tabs.changes.subscribe((value) => { + const matTabsFromQueryList = value.map((tab, index) => { + if (tab.label && tab.iconSrc) { + this.allTabWithLabelAndIcon = true; + } + tab.id = index; + return tab.matTab; + }); + const list = new QueryList(); + list.reset([matTabsFromQueryList]); + this.tabGroup._tabs = list; + this.setActiveTab(); + this.cdRef.detectChanges(); + }); + } + + private hasLabelAndIcon() { + return ( + this.tabs && + this.tabs.filter((tab) => tab.label !== null && tab.iconSrc !== null) + .length > 0 + ); } private generateTabs() { @@ -131,7 +184,8 @@ export class DxcTabsComponent implements OnChanges { } setEventListeners() { - let tabLabels = this._element.nativeElement.getElementsByClassName("mat-tab-label"); + let tabLabels = + this._element.nativeElement.getElementsByClassName("mat-tab-label"); if (tabLabels?.length > 0) { this.tabs.map((tab, index) => { tabLabels[index].addEventListener("click", function () { @@ -145,9 +199,8 @@ export class DxcTabsComponent implements OnChanges { } insertUnderline() { - let tabList = this._element.nativeElement.getElementsByClassName( - "mat-tab-list" - )[0]; + let tabList = + this._element.nativeElement.getElementsByClassName("mat-tab-list")[0]; tabList.insertAdjacentHTML("beforeend", '
'); } @@ -157,24 +210,33 @@ export class DxcTabsComponent implements OnChanges { box-shadow: none; } .mat-tab-list .underline { - height: 1px; + height: var(--tabs-dividerThickness); width: 100%; - background-color: var(--tabs-divider); + background-color: var(--tabs-dividerColor); } .mat-tab-group { + position: relative; + display: flex; + flex-direction: column; ${this.utils.getMargins(inputs.margin)} .mat-tab-header { background-color: white; } + .mat-ink-bar { + background-color: var(--tabs-selectedUnderlineColor); + height: var(--tabs-selectedUnderlineThickness); + } } .mat-tab-list .mat-tab-label { - height: auto !important; - /* max-width: 360px; */ + height: ${this.getTabHeight()} !important; padding-right: 16px; padding-left: 16px; + min-width: 90px; + max-width: 360px; + text-transform: var(--tabs-fontTextTransform) !important; opacity: 1 !important; - /* min-width: 90px; */ - background: var(--tabs-backgroundColor) 0% 0% no-repeat; + color: var(--tabs-unselectedFontColor); + background: var(--tabs-unselectedBackgroundColor) 0% 0% no-repeat; .dxc-tab-label span:not(.show-dot) { opacity: 1; white-space: normal; @@ -182,8 +244,10 @@ export class DxcTabsComponent implements OnChanges { .dxc-tab-label span { color: var(--tabs-fontColor); opacity: 1; - font: normal normal 600 16px/22px var(--fontFamily); - ; + font-family: var(--tabs-fontFamily); + font-size: var(--tabs-fontSize); + font-style: var(--tabs-fontStyle); + font-weight: var(--tabs-fontWeight); } &.cdk-focused { outline: -webkit-focus-ring-color auto 1px; @@ -194,6 +258,7 @@ export class DxcTabsComponent implements OnChanges { fill: var(--tabs-fontColor); } .mat-ripple-element { + font-weight: var(--tabs-pressedFontWeight) !important; background-color: var(--tabs-pressedBackgroundColor); } .mat-tab-label-content { @@ -201,6 +266,13 @@ export class DxcTabsComponent implements OnChanges { display: inline-grid; text-align: -webkit-center; z-index: 1; + + dxc-badge { + position: absolute; + top: 12px; + right: 4px; + } + img, svg { width: 22px; @@ -231,7 +303,6 @@ export class DxcTabsComponent implements OnChanges { flex-direction: column; align-items: center; justify-content: center; - /* min-width: 90px; */ } .only-icon { min-height: 64px; @@ -240,15 +311,8 @@ export class DxcTabsComponent implements OnChanges { display: grid; } } - &.mat-tab-disabled { - .dxc-tab-label span { - color: var(--tabs-disabledFontColor) !important; - } - cursor: not-allowed; - pointer-events: all !important; - } &.mat-tab-label-active { - /* background-color: var(--tabs-backgroundColor); */ + background-color: var(--tabs-selectedBackgroundColor); opacity: 1 !important; .dxc-tab-label span { color: var(--tabs-selectedFontColor); @@ -259,6 +323,18 @@ export class DxcTabsComponent implements OnChanges { fill: var(--tabs-selectedIconColor); } } + &.mat-tab-disabled { + .dxc-tab-label span { + color: var(--tabs-disabledFontColor) !important; + font-style: var(--tabs-disabledFontStyle); + } + dxc-tab-icon { + fill: var(--tabs-disabledIconColor); + } + opacity: 0.5 !important; + cursor: not-allowed; + pointer-events: all !important; + } } &.label-icons { .mat-tab-list .mat-tab-label { @@ -267,4 +343,14 @@ export class DxcTabsComponent implements OnChanges { } `; } + + getTabHeight() { + return ( + ((!this.hasLabelAndIcon || + (this.hasLabelAndIcon && + this.defaultInputs.value.iconPosition !== "top")) && + "48px") || + "72px" + ); + } } diff --git a/projects/dxc-ngx-cdk/src/lib/dxc-tabs/dxc-tabs.module.ts b/projects/dxc-ngx-cdk/src/lib/dxc-tabs/dxc-tabs.module.ts index 34b44c786..162fbc3d1 100644 --- a/projects/dxc-ngx-cdk/src/lib/dxc-tabs/dxc-tabs.module.ts +++ b/projects/dxc-ngx-cdk/src/lib/dxc-tabs/dxc-tabs.module.ts @@ -6,7 +6,9 @@ import { FormsModule } from "@angular/forms"; import { CommonModule } from "@angular/common"; import { DxcTabsComponent } from "./dxc-tabs.component"; import { DxcTabComponent } from "./dxc-tab/dxc-tab.component"; -import { DxcTabIconComponent } from './dxc-tab/dxc-tab-icon/dxc-tab-icon.component'; +import { DxcTabIconComponent } from "./dxc-tab/dxc-tab-icon/dxc-tab-icon.component"; +import { DxcBadgeComponent } from "../dxc-badge/dxc-badge.component"; +import { DxcBadgeModule } from "../dxc-badge/dxc-badge.module"; @NgModule({ declarations: [DxcTabsComponent, DxcTabComponent, DxcTabIconComponent], @@ -15,9 +17,10 @@ import { DxcTabIconComponent } from './dxc-tab/dxc-tab-icon/dxc-tab-icon.compone MatInputModule, MatTabsModule, MatFormFieldModule, + DxcBadgeModule, FormsModule, ], - exports: [DxcTabsComponent, DxcTabComponent,DxcTabIconComponent], + exports: [DxcTabsComponent, DxcTabComponent, DxcTabIconComponent], entryComponents: [MatTab, MatTabGroup], }) export class DxcTabsModule {} diff --git a/projects/dxc-ngx-cdk/src/lib/dxc-tabs/dxc-tabs.types.ts b/projects/dxc-ngx-cdk/src/lib/dxc-tabs/dxc-tabs.types.ts new file mode 100644 index 000000000..6c08ac528 --- /dev/null +++ b/projects/dxc-ngx-cdk/src/lib/dxc-tabs/dxc-tabs.types.ts @@ -0,0 +1,22 @@ +export type Space = + | "xxsmall" + | "xsmall" + | "small" + | "medium" + | "large" + | "xlarge" + | "xxlarge"; + +export type Spacing = { + top?: Space; + bottom?: Space; + left?: Space; + right?: Space; +}; + +export interface TabsProperties { + activeTabIndex?: number; + iconPosition?: "top" | "left"; + margin?: Space | Spacing; + defaultActiveTabIndex?: number; +} diff --git a/projects/dxc-ngx-cdk/src/lib/dxc-wizard/dxc-wizard-step/dxc-wizard-step.component.html b/projects/dxc-ngx-cdk/src/lib/dxc-wizard/dxc-wizard-step/dxc-wizard-step.component.html index f6fa413c8..493257cb1 100644 --- a/projects/dxc-ngx-cdk/src/lib/dxc-wizard/dxc-wizard-step/dxc-wizard-step.component.html +++ b/projects/dxc-ngx-cdk/src/lib/dxc-wizard/dxc-wizard-step/dxc-wizard-step.component.html @@ -10,6 +10,7 @@ [disabled]="disabled" (click)="!disabled ? handleStepClick() : ''" [tabindex]="disabled ? -1 : tabIndexValue" + [attr.aria-current]="isCurrent" >
diff --git a/projects/dxc-ngx-cdk/src/lib/dxc-wizard/dxc-wizard-step/dxc-wizard-step.component.ts b/projects/dxc-ngx-cdk/src/lib/dxc-wizard/dxc-wizard-step/dxc-wizard-step.component.ts index 142805a67..dc06cd6c7 100644 --- a/projects/dxc-ngx-cdk/src/lib/dxc-wizard/dxc-wizard-step/dxc-wizard-step.component.ts +++ b/projects/dxc-ngx-cdk/src/lib/dxc-wizard/dxc-wizard-step/dxc-wizard-step.component.ts @@ -1,4 +1,7 @@ -import { coerceBooleanProperty, coerceNumberProperty } from '@angular/cdk/coercion'; +import { + coerceBooleanProperty, + coerceNumberProperty, +} from "@angular/cdk/coercion"; import { Component, Input, @@ -12,14 +15,24 @@ import { css } from "emotion"; import { BehaviorSubject } from "rxjs"; import { DxcWizardIconComponent } from "../dxc-wizard-icon/dxc-wizard-icon.component"; import { WizardService } from "../services/wizard.service"; +import { WizardStepProperties } from "./dxc-wizard-step.types"; @Component({ selector: "dxc-wizard-step", templateUrl: "./dxc-wizard-step.component.html", }) export class DxcWizardStepComponent { + /** + * Step label. + */ @Input() label: string; + /** + * Description that will be placed next to the step. + */ @Input() description: string; + /** + * Whether the step is disabled or not. + */ @Input() get disabled(): boolean { return this._disabled; @@ -28,6 +41,9 @@ export class DxcWizardStepComponent { this._disabled = coerceBooleanProperty(value); } private _disabled = false; + /** + * Whether the step is valid or not. + */ @Input() get valid(): boolean { return this._valid; @@ -55,7 +71,7 @@ export class DxcWizardStepComponent { @HostBinding("class") className; - defaultInputs = new BehaviorSubject({ + defaultInputs = new BehaviorSubject({ label: null, description: null, disabled: false, @@ -133,22 +149,22 @@ export class DxcWizardStepComponent { .last { flex-grow: 0; margin: ${inputs.mode === "vertical" - ? "25px 0 0 0" - : "0 0 0 25px"} !important; + ? "24px 0 0 0" + : "0 0 0 24px"} !important; &:focus { margin: ${inputs.mode === "vertical" - ? "25px 1px 1px 1px" - : "1px 1px 1px 25px"} !important; + ? "24px 0px 0px 0px" + : "0px 0px 0px 24px"} !important; } } .first { margin: ${inputs.mode === "vertical" - ? "0 0 25px 0" - : "0 25px 0 0"} !important; + ? "0 0 24px 0" + : "0 24px 0 0"} !important; &:focus { margin: ${inputs.mode === "vertical" - ? "1px 1px 25px 1px" - : "1px 25px 1px 1px"} !important; + ? "0px 0px 24px 0px" + : "0px 24px 0px 0px"} !important; } } .step { @@ -157,15 +173,13 @@ export class DxcWizardStepComponent { display: flex; justify-content: flex-start; align-items: center; - margin: ${inputs.mode === "vertical" ? "25px 0" : "0 25px"}; + margin: ${inputs.mode === "vertical" ? "24px 0" : "0 24px"}; padding: 0px; ${inputs.disabled ? "cursor: not-allowed;" : ""} - outline-color: var(--wizard-focusColor); + margin: ${inputs.mode === "vertical" ? "24px 1px" : "1px 24px"}; &:focus { - padding: 2px; - outline: -webkit-focus-ring-color auto 1px; - margin: ${inputs.mode === "vertical" ? "25px 1px" : "1px 25px"}; - outline-color: var(--wizard-focusColor); + outline: var(--wizard-focusColor) auto 1px; + outline-offset: 2px; } &:hover { ${inputs.disabled ? "" : "cursor: pointer;"} @@ -174,42 +188,69 @@ export class DxcWizardStepComponent { .stepHeader { position: relative; display: inline-flex; - padding-bottom: 3px; + padding-bottom: 4px; + } + svg, + img { + width: var(--wizard-stepContainerIconSize); + height: var(--wizard-stepContainerIconSize); + vertical-align: middle; } .iconContainer:not(.current) { - width: ${!inputs.disabled ? "32px" : "36px"}; - height: ${!inputs.disabled ? "32px" : "36px"}; - ${!inputs.disabled ? `border: 2px solid #000000;` : ""} - ${inputs.disabled - ? "background: var(--wizard-disabledBackground) 0% 0% no-repeat padding-box;" - : ""} + width: var(--wizard-circleWidth); + height: var(--wizard-circleHeight); + border: var(--wizard-circleBorderThickness) + var(--wizard-circleBorderStyle) var(--wizard-circleBorderColor); + border-radius: var(--wizard-circleBorderRadius); + background: var(--wizard-stepContainerBackgroundColor); } .current .iconContainer { - width: 36px; - height: 36px; - background: var(--wizard-selectedBackgroundColor) 0% 0% no-repeat - padding-box; - p { - color: var(--wizard-selectedFont) !important; + width: var(--wizard-selectedCircleWidth); + height: var(--wizard-selectedCircleHeight); + border: var(--wizard-selectedCircleBorderThickness) + var(--wizard-selectedCircleBorderStyle) + var(--wizard-selectedCircleBorderColor); + border-radius: var(--wizard-selectedCircleBorderRadius); + background: var(--wizard-stepContainerSelectedBackgroundColor); + .number { + color: var(--wizard-stepContainerSelectedFontColor) !important; + } + svg, + img { + fill: var(--wizard-stepContainerSelectedFontColor); } } .iconContainer { - border-radius: 45px; display: flex; justify-content: center; align-items: center; } - .number:not(.current) { - color: ${!inputs.disabled - ? "var(--wizard-fontColor)" - : "var(--wizard-disabledFont)"}; - } - .current .number { - color: var(--wizard-fontColor); + .disabled { + .iconContainer { + background: var(--wizard-disabledBackgroundColor); + color: var(--wizard-disabledFontColor); + width: var(--wizard-disabledCircleWidth); + height: var(--wizard-disabledCircleHeight); + border: var(--wizard-disabledCircleBorderThickness) + var(--wizard-disabledCircleBorderStyle) + var(--wizard-disabledCircleBorderColor); + border-radius: var(--wizard-disabledCircleBorderRadius); + } + .number { + color: var(--wizard-disabledFontColor); + } + .infoContainer .label, + .infoContainer .description { + color: var(--wizard-disabledFontColor); + } } .number { - font: Normal 16px/22px var(--fontFamily); - letter-spacing: 0.77px; + color: var(--wizard-stepContainerFontColor); + font-family: var(--wizard-stepContainerFontFamily); + font-weight: var(--wizard-stepContainerFontWeight); + font-style: var(--wizard-stepContainerFontStyle); + letter-spacing: var(--wizard-stepContainerLetterSpacing); + font-size: var(--wizard-stepContainerFontSize); opacity: 1; margin: 0; } @@ -217,37 +258,49 @@ export class DxcWizardStepComponent { width: 18px; height: 18px; position: absolute; - bottom: 0px; - right: 0px; + top: 22.5px; + left: 22.5px; } - .infoContainer:not(.visited){ - color: var(--wizard-disabledFont); + .infoContainer { + margin-left: 12px; } - .visited .infoContainer{ - color: var(--wizard-fontColor); + :not(.visited) .label { + color: var(--wizard-labelFontColor); } - .infoContainer { - margin-left: 10px; + .visited .infoContainer .label { + color: var(--wizard-visitedLabelFontColor); } .label { - text-align: left; - font: Normal 16px/22px var(--fontFamily); - letter-spacing: 0.77px; - color: inherit; + text-align: var(--wizard-labelTextAlign); + text-transform: var(--wizard-labelFontTextTransform); + font-size: var(--wizard-labelFontSize); + font-family: var(--wizard-labelFontFamily); + font-weight: var(--wizard-labelFontWeight); + font-style: var(--wizard-labelFontStyle); + letter-spacing: var(--wizard-labelLetterSpacing); margin: 0; } + :not(.visited) .description { + color: var(--wizard-descriptionFontColor); + } + .visited .infoContainer .description { + color: var(--wizard-visitedDescriptionFontColor); + } .description { - text-align: left; - font: Normal 12px/17px var(--fontFamily); - letter-spacing: 0.58px; - color: inherit; + text-align: var(--wizard-descriptionTextAlign); + text-transform: var(--wizard-descriptionFontTextTransform); + font-size: var(--wizard-descriptionFontSize); + font-family: var(--wizard-descriptionFontFamily); + font-weight: var(--wizard-descriptionFontWeight); + font-style: var(--wizard-descriptionFontStyle); + letter-spacing: var(--wizard-descriptionLetterSpacing); margin: 0; } .stepSeparator { width: ${inputs.mode === "horizontal" ? "" : "0"}; height: ${inputs.mode === "horizontal" ? "0" : ""}; ${inputs.mode === "vertical" ? "margin: 0 18px;" : ""} - border: 1px solid var(--wizard-lineColor); + border: var(--wizard-separatorBorderThickness) var(--wizard-separatorBorderStyle) var(--wizard-separatorColor); opacity: 1; flex-grow: 1; } diff --git a/projects/dxc-ngx-cdk/src/lib/dxc-wizard/dxc-wizard-step/dxc-wizard-step.types.ts b/projects/dxc-ngx-cdk/src/lib/dxc-wizard/dxc-wizard-step/dxc-wizard-step.types.ts new file mode 100644 index 000000000..1dbabfb98 --- /dev/null +++ b/projects/dxc-ngx-cdk/src/lib/dxc-wizard/dxc-wizard-step/dxc-wizard-step.types.ts @@ -0,0 +1,7 @@ +export interface WizardStepProperties { + label: string; + description?: string; + disabled?: boolean; + valid?: boolean; + mode?: string; +} diff --git a/projects/dxc-ngx-cdk/src/lib/dxc-wizard/dxc-wizard.component.spec.ts b/projects/dxc-ngx-cdk/src/lib/dxc-wizard/dxc-wizard.component.spec.ts index 84aa32e86..a7964b70c 100644 --- a/projects/dxc-ngx-cdk/src/lib/dxc-wizard/dxc-wizard.component.spec.ts +++ b/projects/dxc-ngx-cdk/src/lib/dxc-wizard/dxc-wizard.component.spec.ts @@ -2,95 +2,174 @@ import { render, fireEvent } from "@testing-library/angular"; import { DxcWizardComponent } from "./dxc-wizard.component"; import { DxcWizardStepComponent } from "./dxc-wizard-step/dxc-wizard-step.component"; import { DxcWizardModule } from "./dxc-wizard.module"; +import { + BrowserDynamicTestingModule, + platformBrowserDynamicTesting, +} from "@angular/platform-browser-dynamic/testing"; +import { TestBed } from "@angular/core/testing"; + +TestBed.initTestEnvironment( + BrowserDynamicTestingModule, + platformBrowserDynamicTesting() +); describe("DxcWizardComponent tests", () => { test("should render dxc-wizard", async () => { - const { getByText } = await render(DxcWizardComponent, { - template: ` - - - `, - imports: [DxcWizardModule], - excludeComponentDeclaration: true, - }); + const { getByText, getAllByRole } = await render( + ` + + + `, + { + imports: [DxcWizardModule], + excludeComponentDeclaration: true, + } + ); expect(getByText("first-step")); expect(getByText("second-step")); + const btn = getAllByRole("button"); + expect(btn[0].getAttribute("aria-current")).toBe("true"); + expect(btn[1].getAttribute("aria-current")).toBe("false"); }); test("click on step text", async () => { const onClickFunction = jest.fn((i) => null); - const { getByText } = await render(DxcWizardComponent, { - template: ` - - - `, - componentProperties: { - onClickFunction, - }, - imports: [DxcWizardModule], - excludeComponentDeclaration: true, - }); + const { getByText, getAllByRole } = await render( + ` + + + `, + { + componentProperties: { + onClickFunction, + }, + imports: [DxcWizardModule], + excludeComponentDeclaration: true, + } + ); const step = getByText("first-step"); fireEvent.click(step); expect(onClickFunction).toHaveBeenCalledWith(0); + const btn = getAllByRole("button"); + expect(btn[0].getAttribute("aria-current")).toBe("true"); + expect(btn[1].getAttribute("aria-current")).toBe("false"); }); - test("click on step text", async () => { + test("click on step text with description", async () => { const onClickFunction = jest.fn((i) => null); - const { getByText } = await render(DxcWizardComponent, { - template: ` - - - `, - componentProperties: { - onClickFunction, - }, - imports: [DxcWizardModule], - excludeComponentDeclaration: true, - }); + const { getByText, getAllByRole } = await render( + ` + + + `, + { + componentProperties: { + onClickFunction, + }, + imports: [DxcWizardModule], + excludeComponentDeclaration: true, + } + ); const step = getByText("step-description"); fireEvent.click(step); expect(onClickFunction).toHaveBeenCalledWith(1); + const btn = getAllByRole("button"); + expect(btn[0].getAttribute("aria-current")).toBe("false"); + expect(btn[1].getAttribute("aria-current")).toBe("true"); }); test("click on step number", async () => { const onClickFunction = jest.fn((i) => null); - const { getByText } = await render(DxcWizardComponent, { - template: ` - - - `, - componentProperties: { - onClickFunction, - }, - imports: [DxcWizardModule], - excludeComponentDeclaration: true, - }); + const { getByText, getAllByRole } = await render( + ` + + + `, + { + componentProperties: { + onClickFunction, + }, + imports: [DxcWizardModule], + excludeComponentDeclaration: true, + } + ); const step = getByText("1"); fireEvent.click(step); expect(onClickFunction).toHaveBeenCalledWith(0); + const btn = getAllByRole("button"); + expect(btn[0].getAttribute("aria-current")).toBe("true"); + expect(btn[1].getAttribute("aria-current")).toBe("false"); }); - test("click on disable step", async () => { + test("click on disabled step", async () => { const onClickFunction = jest.fn((i) => null); - const { getByText } = await render(DxcWizardComponent, { - template: ` - - - `, - componentProperties: { - onClickFunction, - }, - imports: [DxcWizardModule], - excludeComponentDeclaration: true, - }); + const { getByText, getAllByRole } = await render( + ` + + + `, + { + componentProperties: { + onClickFunction, + }, + imports: [DxcWizardModule], + excludeComponentDeclaration: true, + } + ); const step = getByText("second-step"); fireEvent.click(step); expect(onClickFunction).toHaveBeenCalledTimes(0); + const btn = getAllByRole("button"); + expect(btn[0].getAttribute("aria-current")).toBe("true"); + expect(btn[1].getAttribute("aria-current")).toBe("false"); + }); + + test("wizard with default step", async () => { + const onClickFunction = jest.fn((i) => null); + const { getAllByRole } = await render( + ` + + + + `, + { + componentProperties: { + onClickFunction, + }, + imports: [DxcWizardModule], + excludeComponentDeclaration: true, + } + ); + const btn = getAllByRole("button"); + expect(btn[0].getAttribute("aria-current")).toBe("false"); + expect(btn[1].getAttribute("aria-current")).toBe("false"); + expect(btn[2].getAttribute("aria-current")).toBe("true"); + }); + + test("wizard with default step and current step", async () => { + const onClickFunction = jest.fn((i) => null); + const { getAllByRole } = await render( + ` + + + + `, + { + componentProperties: { + onClickFunction, + }, + imports: [DxcWizardModule], + excludeComponentDeclaration: true, + } + ); + const btn = getAllByRole("button"); + expect(btn[0].getAttribute("aria-current")).toBe("false"); + expect(btn[1].getAttribute("aria-current")).toBe("true"); + expect(btn[2].getAttribute("aria-current")).toBe("false"); }); }); diff --git a/projects/dxc-ngx-cdk/src/lib/dxc-wizard/dxc-wizard.component.ts b/projects/dxc-ngx-cdk/src/lib/dxc-wizard/dxc-wizard.component.ts index 36ae26b5b..137c471ee 100644 --- a/projects/dxc-ngx-cdk/src/lib/dxc-wizard/dxc-wizard.component.ts +++ b/projects/dxc-ngx-cdk/src/lib/dxc-wizard/dxc-wizard.component.ts @@ -14,7 +14,8 @@ import { CssUtils } from "../utils"; import { DxcWizardStepComponent } from "./dxc-wizard-step/dxc-wizard-step.component"; import { WizardService } from "./services/wizard.service"; import { ChangeDetectorRef } from "@angular/core"; -import { coerceNumberProperty } from '@angular/cdk/coercion'; +import { coerceNumberProperty } from "@angular/cdk/coercion"; +import { Spacing, Space, WizardProperties } from "./dxc-wizard.types"; @Component({ selector: "dxc-wizard", @@ -22,9 +23,41 @@ import { coerceNumberProperty } from '@angular/cdk/coercion'; providers: [CssUtils, WizardService], }) export class DxcWizardComponent { - @Input() mode: string = "horizontal"; - @Input() currentStep: number; - @Input() margin: any; + /** + * The wizard can be showed in horizontal or vertical. + */ + @Input() mode: "horizontal" | "vertical" = "horizontal"; + /** + * Defines which step is marked as the current. The numeration starts at 0. + * If undefined, the component will be uncontrolled and the step will be managed internally by the component. + */ + @Input() + get currentStep(): number { + return this._currentStep; + } + set currentStep(value: number) { + this._currentStep = coerceNumberProperty(value); + } + private _currentStep = 0; + /** + * Initially selected step, only when it is uncontrolled. + */ + @Input() + get defaultCurrentStep(): number { + return this._defaultCurrentStep; + } + set defaultCurrentStep(value: number) { + this._defaultCurrentStep = coerceNumberProperty(value); + } + private _defaultCurrentStep = 0; + /** + * Size of the margin to be applied to the component ('xxsmall' | 'xsmall' | 'small' | 'medium' | 'large' | 'xlarge' | 'xxlarge'). + * You can pass an object with 'top', 'bottom', 'left' and 'right' properties in order to specify different margin sizes. + */ + @Input() margin: Spacing | Space; + /** + * Value of the tabindex attribute that is given to all the steps. + */ @Input() get tabIndexValue(): number { return this._tabIndexValue; @@ -32,18 +65,24 @@ export class DxcWizardComponent { set tabIndexValue(value: number) { this._tabIndexValue = coerceNumberProperty(value); } - private _tabIndexValue; - @Output() onStepClick = new EventEmitter(); + private _tabIndexValue = 0; + /** + * This event will emit in case the user clicks a step. The step + * number will be passed as a parameter. + */ + @Output() onStepClick: EventEmitter = new EventEmitter(); @ContentChildren(DxcWizardStepComponent) dxcWizardSteps: QueryList; @HostBinding("class") className; - defaultInputs = new BehaviorSubject({ + defaultInputs = new BehaviorSubject({ mode: "horizontal", - currentStep: null, + currentStep: 0, margin: null, + tabIndexValue: 0, + defaultCurrentStep: 0, }); constructor( @@ -54,6 +93,7 @@ export class DxcWizardComponent { ngAfterViewInit(): void { this.service.setSteps(this.dxcWizardSteps); + this.service.innerCurrentStep.next(this.currentStep); this.service.newCurrentStep.subscribe((value) => { if (value || value === 0) { this.handleStepClick(value); @@ -67,7 +107,10 @@ export class DxcWizardComponent { ngOnInit() { this.className = `${this.getDynamicStyle(this.defaultInputs.getValue())}`; - this.service.innerCurrentStep.next(this.currentStep || 0); + this.currentStep = this.currentStep + ? this.currentStep + : this.defaultCurrentStep ?? 0; + this.service.innerCurrentStep.next(this.currentStep); this.service.mode.next(this.mode || "horizontal"); this.service.tabIndexValue.next(this.tabIndexValue); } @@ -86,9 +129,7 @@ export class DxcWizardComponent { } public handleStepClick(i) { - if (!(this.currentStep || this.currentStep === 0)) { - this.service.innerCurrentStep.next(i); - } + this.service.innerCurrentStep.next(i); this.onStepClick.emit(i); } @@ -99,56 +140,6 @@ export class DxcWizardComponent { flex-direction: ${inputs.mode === "vertical" ? "column" : "row"}; justify-content: center; ${inputs.mode === "vertical" ? "height: 500px" : "width: 100%"}; - - svg, - img { - width: 19px; - height: 19px; - vertical-align: middle; - } - - dxc-wizard-step { - :not(.current, .disabled) { - .iconContainer { - width: 32px; - height: 32px; - border: 2px solid var(--wizard-borderColor); - } - .number { - color: var(--wizard-fontColor); - font-family: var(--fontFamily); - } - } - - .current, - .disabled { - .iconContainer { - width: 36px; - height: 36px; - border: none; - } - } - - .current { - .iconContainer { - background: var(--wizard-selectedBackgroundColor); - color: var(--wizard-selectedFont); - } - .number { - color: var(--wizard-fontColor); - } - } - - .disabled { - .iconContainer { - background: var(--wizard-disabledBackground); - color: var(--wizard-disabledFont); - } - .number { - color: var(--wizard-disabledFont); - } - } - } `; } } diff --git a/projects/dxc-ngx-cdk/src/lib/dxc-wizard/dxc-wizard.types.ts b/projects/dxc-ngx-cdk/src/lib/dxc-wizard/dxc-wizard.types.ts new file mode 100644 index 000000000..b31f11be1 --- /dev/null +++ b/projects/dxc-ngx-cdk/src/lib/dxc-wizard/dxc-wizard.types.ts @@ -0,0 +1,23 @@ +export interface WizardProperties { + mode?: string; + currentStep?: number; + margin?: Spacing | Space; + defaultCurrentStep?: number; + tabIndexValue?: number; +} + +export type Space = + | "xxsmall" + | "xsmall" + | "small" + | "medium" + | "large" + | "xlarge" + | "xxlarge"; + +export type Spacing = { + top?: Space; + bottom?: Space; + left?: Space; + right?: Space; +}; diff --git a/projects/dxc-ngx-cdk/src/lib/dxc-wizard/services/wizard.service.ts b/projects/dxc-ngx-cdk/src/lib/dxc-wizard/services/wizard.service.ts index 175b43667..63519dda1 100644 --- a/projects/dxc-ngx-cdk/src/lib/dxc-wizard/services/wizard.service.ts +++ b/projects/dxc-ngx-cdk/src/lib/dxc-wizard/services/wizard.service.ts @@ -17,9 +17,9 @@ export class WizardService { public tabIndexValue: BehaviorSubject = new BehaviorSubject(0); constructor() { - this.innerCurrentStep.subscribe((newCurrent) => { + this.innerCurrentStep.subscribe((newCurrent: number) => { if (this.steps && (newCurrent || newCurrent === 0)) { - this.steps.forEach((element, index) => { + this.steps.forEach((element: DxcWizardStepComponent, index: number) => { element.setIsCurrent(index === newCurrent, newCurrent); }); } @@ -33,7 +33,10 @@ export class WizardService { element.isFirst = index === 0; element.setIsLast(index === this.steps.length - 1); element.position = index; - element.setIsCurrent(index === this.innerCurrentStep.value, this.innerCurrentStep.value); + element.setIsCurrent( + index === this.innerCurrentStep.value, + this.innerCurrentStep.value + ); }); } } diff --git a/projects/dxc-ngx-cdk/src/lib/utils.ts b/projects/dxc-ngx-cdk/src/lib/utils.ts index 4b2caaf28..d720c4aa3 100644 --- a/projects/dxc-ngx-cdk/src/lib/utils.ts +++ b/projects/dxc-ngx-cdk/src/lib/utils.ts @@ -21,6 +21,10 @@ export class CssUtils { `; } + getMarginValue = (margin, type) => { + const marginSize = margin && margin !== null ? spaces[margin[type]] : "0px"; + return marginSize; + }; getTopMargin(margin) { return margin && typeof margin !== "object" ? css` @@ -168,24 +172,42 @@ export class CssUtils { return value; } - getBoxShadow(shadowDepth) { + getBoxShadow(shadowDepth, isImportant: boolean = false) { switch (shadowDepth) { - case "1": + case 1: return css` - box-shadow: 0px 2px 1px -1px rgba(0, 0, 0, 0.2), - 0px 1px 1px 0px rgba(0, 0, 0, 0.14), - 0px 1px 3px 0px rgba(0, 0, 0, 0.12); + box-shadow: var(--box-oneShadowDepthShadowOffsetX) + var(--box-oneShadowDepthShadowOffsetY) + var(--box-oneShadowDepthShadowBlur) + var(--box-oneShadowDepthShadowSpread) + var(--box-oneShadowDepthShadowColor) + ${this.isPropertyImportant(isImportant)}; `; - case "2": + case 2: return css` - box-shadow: 0px 3px 3px -2px rgba(0, 0, 0, 0.2), - 0px 3px 4px 0px rgba(0, 0, 0, 0.14), - 0px 1px 8px 0px rgba(0, 0, 0, 0.12); + box-shadow: var(--box-twoShadowDepthShadowOffsetX) + var(--box-twoShadowDepthShadowOffsetY) + var(--box-twoShadowDepthShadowBlur) + var(--box-twoShadowDepthShadowSpread) + var(--box-twoShadowDepthShadowColor) + ${this.isPropertyImportant(isImportant)}; `; default: return css` - box-shadow: none; + box-shadow: var(--box-noneShadowDepthShadowOffsetX) + var(--box-noneShadowDepthShadowOffsetY) + var(--box-noneShadowDepthShadowBlur) + var(--box-noneShadowDepthShadowSpread) + var(--box-noneShadowDepthShadowColor) + ${this.isPropertyImportant(isImportant)}; `; } } + readProperty(name: string): string { + let bodyStyles = window.getComputedStyle(document.body); + return bodyStyles.getPropertyValue(name); + } + private isPropertyImportant(isImportant) { + return isImportant ? " !important" : ""; + } }