Skip to content
Merged
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
16 changes: 13 additions & 3 deletions libs/core/object-number/object-number.component.html
Original file line number Diff line number Diff line change
@@ -1,4 +1,14 @@
<span class="fd-object-number__text" [class.fd-object-number__text--bold]="!large && emphasized">
{{ number | number: (decimal ? _numberPipeConfig : '1.0-0') }}
<span class="fd-object-number__text" [class.fd-object-number__text--bold]="!large() && emphasized()">
{{ number() | number: (decimal() ? _numberPipeConfig : '1.0-0') }}
</span>
<span class="fd-object-number__unit">{{ unit }}</span>
<span class="fd-object-number__unit">{{ unit() }}</span>
@if (emphasized()) {
<span class="fd-object-number__sr-only">{{ 'coreObjectNumber.emphasized' | fdTranslate }}</span>
}
@if (status()) {
@if (statusMessage()) {
<span class="fd-object-number__sr-only">{{ statusMessage() }}</span>
} @else {
<span class="fd-object-number__sr-only">{{ statusKey() | fdTranslate }}</span>
}
}
128 changes: 91 additions & 37 deletions libs/core/object-number/object-number.component.spec.ts
Original file line number Diff line number Diff line change
@@ -1,45 +1,41 @@
import { Component, ViewChild } from '@angular/core';
import { Pipe, PipeTransform } from '@angular/core';
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';

import { FdTranslatePipe } from '@fundamental-ngx/i18n';
import { ObjectNumberComponent } from './object-number.component';

@Component({
selector: 'fd-test-object-number',
template: ` <fd-object-number
[number]="1000.37"
[unit]="unit"
[decimal]="decimal"
[large]="large"
[status]="status"
[class]="class"
></fd-object-number>`,
standalone: true,
imports: [ObjectNumberComponent]
@Pipe({
name: 'fdTranslate',
standalone: true
})
class TestObjectNumberComponent {
@ViewChild(ObjectNumberComponent, { static: true })
objectNumberComponent: ObjectNumberComponent;
unit = 'EUR';
decimal = 0;
large = false;
status = '';
class = '';
class MockFdTranslatePipe implements PipeTransform {
transform(value: string): string {
// For testing, just return the translation key itself
// This way we can verify the correct keys are being used
return value;
}
}

describe('ObjectNumberComponent', () => {
let component: ObjectNumberComponent;
let fixture: ComponentFixture<TestObjectNumberComponent>;
let fixture: ComponentFixture<ObjectNumberComponent>;
const number = 1000.37;
const numberTextEl = '.fd-object-number__text';

beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
imports: [TestObjectNumberComponent]
}).compileComponents();
imports: [ObjectNumberComponent]
})
.overrideComponent(ObjectNumberComponent, {
remove: { imports: [FdTranslatePipe] },
add: { imports: [MockFdTranslatePipe] }
})
.compileComponents();
}));

beforeEach(() => {
fixture = TestBed.createComponent(TestObjectNumberComponent);
component = fixture.componentInstance.objectNumberComponent;
fixture = TestBed.createComponent(ObjectNumberComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});

Expand All @@ -48,40 +44,98 @@ describe('ObjectNumberComponent', () => {
});

it('should apply large design', () => {
fixture.componentInstance.large = true;
fixture.componentRef.setInput('large', true);
fixture.detectChanges();
expect(component.elementRef.nativeElement.classList.contains('fd-object-number--large')).toBe(true);
});

it('should add status', () => {
fixture.componentInstance.status = 'positive';
fixture.detectChanges();
expect(component.elementRef.nativeElement.classList.contains('fd-object-number--positive')).toBe(true);
});

it('should add custom class', () => {
fixture.componentInstance.class = 'custom-class';
fixture.componentRef.setInput('class', 'custom-class');
fixture.detectChanges();
expect(component.elementRef.nativeElement.classList.contains('custom-class')).toBe(true);
});

it('should display units', () => {
fixture.componentInstance.unit = 'TEST';
fixture.componentRef.setInput('unit', 'TEST');
fixture.detectChanges();
expect(component.elementRef.nativeElement.textContent.includes('TEST')).toBe(true);
});

it('should display decimals', () => {
fixture.componentInstance.decimal = 2;
fixture.componentRef.setInput('number', number);
fixture.componentRef.setInput('decimal', 2);
fixture.detectChanges();
expect(
component.elementRef.nativeElement.querySelector(numberTextEl).textContent.includes('1,000.37')
).toBeTruthy();
});

it('should not display decimals if [decimal] set to 0', () => {
fixture.componentInstance.decimal = 0;
fixture.componentRef.setInput('number', number);
fixture.componentRef.setInput('decimal', 0);
fixture.detectChanges();
expect(component.elementRef.nativeElement.querySelector(numberTextEl).textContent.trim()).toEqual('1,000');
});

it('should add translated screen reader "Emphasized" text when object number is emphasized', () => {
fixture.componentRef.setInput('emphasized', true);
fixture.detectChanges();
expect(component.elementRef.nativeElement.querySelector('.fd-object-number__sr-only').textContent).toEqual(
'coreObjectNumber.emphasized'
);
});

describe('when status is set', () => {
beforeEach(() => {
fixture.componentRef.setInput('status', 'positive');
fixture.detectChanges();
});

it('should add the correct status class', () => {
expect(component.elementRef.nativeElement.classList.contains('fd-object-number--positive')).toBe(true);
});

describe('when no statusMessage is provided', () => {
it('should add default translated screen reader status text', () => {
expect(fixture.nativeElement.querySelector('.fd-object-number__sr-only').textContent).toEqual(
'coreObjectNumber.positive'
);
});
});

describe('when statusMessage is provided', () => {
it('should add the provided statusMessage as screen reader text', () => {
fixture.componentRef.setInput('statusMessage', 'Custom status message');
fixture.detectChanges();
expect(fixture.nativeElement.querySelector('.fd-object-number__sr-only').textContent).toEqual(
'Custom status message'
);
});
});
});

describe('when interactive is true', () => {
beforeEach(() => {
fixture.componentRef.setInput('interactive', true);
fixture.detectChanges();
});

it('should add an "fd-object-number--interactive" class', () => {
expect(component.elementRef.nativeElement.classList.contains('fd-object-number--interactive')).toBe(true);
});

it('should set tabindex on the host to 0', () => {
expect(component.elementRef.nativeElement.tabIndex).toBe(0);
});

it('should set role on the host to "button"', () => {
expect(component.elementRef.nativeElement.role).toBe('button');
});
});

it('should add an "fd-object-number--inverted" class when inverted is true', () => {
fixture.componentRef.setInput('inverted', true);
fixture.detectChanges();
expect(component.elementRef.nativeElement.classList.contains('fd-object-number--inverted')).toBe(true);
});
});
78 changes: 51 additions & 27 deletions libs/core/object-number/object-number.component.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,17 @@
import { DecimalPipe } from '@angular/common';
import {
booleanAttribute,
ChangeDetectionStrategy,
Component,
computed,
ElementRef,
Input,
input,
OnChanges,
OnInit,
ViewEncapsulation
} from '@angular/core';
import { CssClassBuilder, Nullable, applyCssClass } from '@fundamental-ngx/cdk/utils';
import { applyCssClass, CssClassBuilder, Nullable } from '@fundamental-ngx/cdk/utils';
import { FdLanguageKeyIdentifier, FdTranslatePipe } from '@fundamental-ngx/i18n';

type ObjectStatus = 'negative' | 'critical' | 'positive' | 'informative';

Expand All @@ -17,53 +20,64 @@ type ObjectStatus = 'negative' | 'critical' | 'positive' | 'informative';
templateUrl: './object-number.component.html',
styleUrl: './object-number.component.scss',
host: {
'[attr.aria-labelledby]': 'ariaLabelledBy',
'[attr.aria-label]': 'ariaLabel'
'[attr.aria-labelledby]': 'ariaLabelledBy()',
'[attr.aria-label]': 'ariaLabel()',
'[attr.tabindex]': 'interactive() ? 0 : null',
'[attr.role]': 'interactive() ? "button" : null'
},
encapsulation: ViewEncapsulation.None,
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [DecimalPipe]
imports: [DecimalPipe, FdTranslatePipe]
})
export class ObjectNumberComponent implements OnInit, OnChanges, CssClassBuilder {
/**
* Numerical value of the object number.
*/
@Input()
number: number;
number = input<number>(0);

/**
* Number of decimal places to show
*/
@Input()
decimal = 0;
decimal = input<number>(0);

/** Sets unit of measure displayed. */
@Input()
unit: string;
unit = input<string>('');

/** Set the value to true to display the object number in bold text */
@Input()
emphasized = false;
emphasized = input(false, { transform: booleanAttribute });

/** Set the value to true to display the object number in large text */
@Input()
large = false;
large = input(false, { transform: booleanAttribute });

/** Sets status/semantic color 'negative' / 'critical' / 'positive' / 'informative' */
@Input()
status: ObjectStatus;
status = input<ObjectStatus | null>(null);

/** An optional status message for the object number */
statusMessage = input<string>();

/** Status key to translate for screen readers */
statusKey = computed<FdLanguageKeyIdentifier | null>(() => {
const status = this.status();
if (this.isValidObjectStatus(status)) {
return `coreObjectNumber.${status}`;
}
return null;
});

/** User's custom classes */
@Input()
class: string;
class = input<string>();

/** Id of the element that labels object number. */
@Input()
ariaLabelledBy: Nullable<string>;
ariaLabelledBy = input<Nullable<string>>(null);

/** Aria label for the object number. */
@Input()
ariaLabel: Nullable<string>;
ariaLabel = input<Nullable<string>>(null);

/** Whether the object number is interactive */
interactive = input(false, { transform: booleanAttribute });

/** Whether the object number is inverted. */
inverted = input(false, { transform: booleanAttribute });

/** @hidden */
_numberPipeConfig = '';
Expand All @@ -80,9 +94,11 @@ export class ObjectNumberComponent implements OnInit, OnChanges, CssClassBuilder
buildComponentCssClass(): string[] {
return [
'fd-object-number',
this.large ? 'fd-object-number--large' : '',
this.status ? `fd-object-number--${this.status}` : '',
this.class
this.large() ? 'fd-object-number--large' : '',
this.status() ? `fd-object-number--${this.status()}` : '',
this.interactive() ? 'fd-object-number--interactive' : '',
this.inverted() ? 'fd-object-number--inverted' : '',
this.class() ?? ''
];
}

Expand All @@ -104,6 +120,14 @@ export class ObjectNumberComponent implements OnInit, OnChanges, CssClassBuilder

/** @hidden */
private _buildNumberPipeConfig(): void {
this._numberPipeConfig = `0.${this.decimal}-${this.decimal}`;
this._numberPipeConfig = `0.${this.decimal()}-${this.decimal()}`;
}

/**
* Type guard to check if the status is a valid ObjectStatus
* @hidden
*/
private isValidObjectStatus(status: ObjectStatus | null): status is ObjectStatus {
return status === 'negative' || status === 'critical' || status === 'positive' || status === 'informative';
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -56,3 +56,27 @@ export class ObjectNumberDecimalExampleComponent {}
imports: [ObjectNumberModule]
})
export class ObjectNumberTruncationExampleComponent {}

@Component({
selector: 'fd-object-number-interactive-example',
templateUrl: './object-number-interactive-example.component.html',
styleUrls: ['./object-number-examples.component.scss'],
imports: [ObjectNumberModule]
})
export class ObjectNumberInteractiveExampleComponent {}

@Component({
selector: 'fd-object-number-inverted-example',
templateUrl: './object-number-inverted-example.component.html',
styleUrls: ['./object-number-examples.component.scss'],
imports: [ObjectNumberModule]
})
export class ObjectNumberInvertedExampleComponent {}

@Component({
selector: 'fd-object-number-inverted-interactive-example',
templateUrl: './object-number-inverted-interactive-example.component.html',
styleUrls: ['./object-number-examples.component.scss'],
imports: [ObjectNumberModule]
})
export class ObjectNumberInvertedInteractiveExampleComponent {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<fd-object-number
[number]="-2000"
unit="EUR"
status="negative"
[interactive]="true"
[emphasized]="true"
[decimal]="2"
></fd-object-number>
<fd-object-number [number]="0" unit="EUR" status="critical" [interactive]="true" [decimal]="2"></fd-object-number>
<fd-object-number
[number]="1000"
unit="EUR"
status="positive"
[interactive]="true"
[emphasized]="true"
[decimal]="2"
></fd-object-number>
<fd-object-number [number]="100" unit="EUR" status="informative" [interactive]="true" [decimal]="2"></fd-object-number>
<fd-object-number [number]="999" unit="EUR" [interactive]="true" [decimal]="2"></fd-object-number>
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<fd-object-number [number]="-2000" unit="EUR" status="negative" [inverted]="true" [decimal]="2"></fd-object-number>
<fd-object-number [number]="0" unit="EUR" status="critical" [inverted]="true" [decimal]="2"></fd-object-number>
<fd-object-number [number]="1000" unit="EUR" status="positive" [inverted]="true" [decimal]="2"></fd-object-number>
<fd-object-number [number]="100" unit="EUR" status="informative" [inverted]="true" [decimal]="2"></fd-object-number>
<fd-object-number [number]="999" unit="EUR" [inverted]="true" [decimal]="2"></fd-object-number>
Loading