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
19 changes: 17 additions & 2 deletions projects/components/src/button/button.component.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { TrackDirective } from '@hypertrace/common';
import { createHostFactory, Spectator } from '@ngneat/spectator/jest';
import { MockDirective } from 'ng-mocks';
import { IconSize } from '../icon/icon-size';
import { ButtonRole, ButtonSize, ButtonStyle } from './button';
import { ButtonRole, ButtonSize, ButtonStyle, ButtonType } from './button';
import { ButtonComponent } from './button.component';
import { ButtonModule } from './button.module';

Expand All @@ -26,7 +26,22 @@ describe('Button Component', () => {
}
});

expect(spectator.query('.button')).toHaveClass('button secondary small solid');
const buttonEl = spectator.query('.button');
expect(buttonEl).toHaveClass('button secondary small solid');
expect(buttonEl).toHaveAttribute('type', 'button');
});

test('should set correct button type', () => {
spectator = createHost(`<ht-button [label]="label" [type]="type"></ht-button>`, {
hostProps: {
label: 'Button',
type: ButtonType.Submit
}
});

const buttonEl = spectator.query('.button');
expect(buttonEl).toHaveClass('button secondary small solid');
expect(buttonEl).toHaveAttribute('type', 'submit');
});

test('should have correct style class for selected role', () => {
Expand Down
13 changes: 11 additions & 2 deletions projects/components/src/button/button.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,21 @@ import { ChangeDetectionStrategy, Component, Input } from '@angular/core';
import { IconType } from '@hypertrace/assets-library';
import { isNil } from 'lodash-es';
import { IconSize } from '../icon/icon-size';
import { ButtonRole, ButtonSize, ButtonStyle } from './button';
import { ButtonRole, ButtonSize, ButtonStyle, ButtonType } from './button';

@Component({
selector: 'ht-button',
styleUrls: ['./button.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<ht-event-blocker event="click" class="button-container" [enabled]="this.disabled">
<button class="button" [ngClass]="this.getStyleClasses()" [htTrack] [htTrackLabel]="this.label">
<button
class="button"
[ngClass]="this.getStyleClasses()"
[htTrack]
[htTrackLabel]="this.label"
[type]="this.type"
>
<ht-icon
*ngIf="this.icon && !this.trailingIcon"
[icon]="this.icon"
Expand Down Expand Up @@ -45,6 +51,9 @@ export class ButtonComponent {
@Input()
public trailingIcon?: boolean;

@Input()
public type: ButtonType = ButtonType.Button;

@Input()
public role: ButtonRole = ButtonRole.Secondary;

Expand Down
5 changes: 5 additions & 0 deletions projects/components/src/button/button.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,3 +26,8 @@ export const enum ButtonStyle {
Text = 'text', // No No Yes No Yes
PlainText = 'plain-text' // No No Yes No No
}

export const enum ButtonType {
Button = 'button',
Submit = 'submit'
}
28 changes: 28 additions & 0 deletions projects/components/src/form-field/form-field.component.scss
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
@import 'mixins';

.form-field {
display: flex;
flex-direction: column;

&.optional {
margin-bottom: 18px;
}
Expand All @@ -26,6 +29,31 @@
}
}

.content {
flex: 1 1 auto;
width: 100%;
border: 1px solid $gray-2;
border-radius: 6px;
width: 100%;
background: white;

&.error-border {
border: 1px solid $red-3;
}
}

.error {
display: flex;
align-items: center;
color: $red-6;
margin-top: 4px;

.error-label {
@include footnote($red-6);
margin-left: 4px;
}
}

.error-message {
@include footnote($red-6);
}
Expand Down
28 changes: 28 additions & 0 deletions projects/components/src/form-field/form-field.component.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,4 +54,32 @@ describe('Form Field Component', () => {
expect(labels[0].label).toEqual('Label');
expect(labels[1].label).toEqual('(Optional)');
});

test('should show error when showFormError and errorLabel is present', () => {
const spectator = hostFactory(
`
<ht-form-field [label]="label" [showFormError]="showFormError" [errorLabel]="errorLabel">
</ht-form-field>`,
{
hostProps: {
label: 'Label',
errorLabel: 'Invalid Form element',
showFormError: true
}
}
);

expect(spectator.query('.error')).toExist();

const labels = spectator.queryAll(LabelComponent);
expect(labels[0].label).toEqual('Label');
// Error label is shown when showFormError is true
expect(labels[1].label).toEqual('Invalid Form element');

spectator.setHostInput({
showFormError: false
});

expect(spectator.query('.error')).not.toExist();
});
});
21 changes: 19 additions & 2 deletions projects/components/src/form-field/form-field.component.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { ChangeDetectionStrategy, Component, Input } from '@angular/core';
import { IconType } from '@hypertrace/assets-library';
import { IconSize } from '../icon/icon-size';

@Component({
Expand All @@ -18,8 +19,21 @@ import { IconSize } from '../icon/icon-size';
[htTooltip]="this.iconTooltip"
></ht-icon>
</div>
<ng-content></ng-content>
<ht-label *ngIf="!this.isOptional" class="error-message" [label]="this.errorLabel"></ht-label>
<div class="content" [ngClass]="{ 'error-border': this.showFormError && this.errorLabel }">
<ng-content></ng-content>
</div>
<!-- For Backward Compatibility: Start -->
<ht-label
*ngIf="!this.isOptional && this.showFormError === undefined"
class="error-message"
[label]="this.errorLabel"
></ht-label>
<!-- For Backward Compatibility: End -->

<div class="error" *ngIf="this.showFormError && this.errorLabel">
<ht-icon icon="${IconType.Error}" size="${IconSize.Small}"></ht-icon>
<ht-label class="error-label" [label]="this.errorLabel"></ht-label>
</div>
</section>
`
})
Expand All @@ -41,4 +55,7 @@ export class FormFieldComponent {

@Input()
public errorLabel?: string = '';

@Input()
public showFormError?: boolean = true;
}
36 changes: 33 additions & 3 deletions projects/components/src/input/input.component.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,19 @@
import { ChangeDetectionStrategy, Component, EventEmitter, Input, OnChanges, Output } from '@angular/core';
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
import { NumberCoercer, TypedSimpleChanges } from '@hypertrace/common';
import { InputAppearance } from './input-appearance';

@Component({
selector: 'ht-input',
styleUrls: ['./input.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
providers: [
{
provide: NG_VALUE_ACCESSOR,
multi: true,
useExisting: InputComponent
}
],
template: `
<mat-form-field [ngClass]="this.getStyleClasses()" floatLabel="never">
<input
Expand All @@ -20,7 +28,7 @@ import { InputAppearance } from './input-appearance';
</mat-form-field>
`
})
export class InputComponent<T extends string | number> implements OnChanges {
export class InputComponent<T extends string | number> implements ControlValueAccessor, OnChanges {
@Input()
public placeholder?: string;

Expand Down Expand Up @@ -52,10 +60,31 @@ export class InputComponent<T extends string | number> implements OnChanges {
}
}

private propagateControlValueChange?: (value: T | undefined) => void;
private propagateControlValueChangeOnTouch?: (value: T | undefined) => void;

public onValueChange(value?: string): void {
const coercedValue = this.coerceValueIfNeeded(value);
this.value = coercedValue;
this.valueChange.emit(coercedValue);
this.propagateValueChangeToFormControl(coercedValue);
}

public getStyleClasses(): string[] {
return [this.appearance, this.disabled ? 'disabled' : ''];
}

public writeValue(value?: string): void {
const coercedValue = this.coerceValueIfNeeded(value);
this.value = coercedValue;
}

public registerOnChange(onChange: (value: T | undefined) => void): void {
this.propagateControlValueChange = onChange;
}

public registerOnTouched(onTouch: (value: T | undefined) => void): void {
this.propagateControlValueChangeOnTouch = onTouch;
}

private coerceValueIfNeeded(value?: string): T | undefined {
Expand All @@ -67,7 +96,8 @@ export class InputComponent<T extends string | number> implements OnChanges {
}
}

public getStyleClasses(): string[] {
return [this.appearance, this.disabled ? 'disabled' : ''];
private propagateValueChangeToFormControl(value: T | undefined): void {
this.propagateControlValueChange?.(value);
this.propagateControlValueChangeOnTouch?.(value);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,7 @@ describe('Modal Container component', () => {
content: TestComponent,
size: ModalSize.LargeTall
});
checkSyles('640px', '800px');
checkSyles('640px', '90vh');
});

test('uses the requested medium-wide size', () => {
Expand Down
7 changes: 5 additions & 2 deletions projects/components/src/modal/modal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,15 +36,18 @@ export const getModalDimensions = (modalSize: ModalSize): ModalDimension => {
case ModalSize.Large:
return getModalDimensionObject(640, 720);
case ModalSize.LargeTall:
return getModalDimensionObject(640, 800);
return getModalDimensionObject(640, '90vh');
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

please ensure you test the change across various modals in the system today.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I did check a few modals, but it shouldn't be impactful anyway. Just making a better use of space when it is available.

case ModalSize.MediumWide:
return getModalDimensionObject(840, 600);
default:
return getModalDimensionObject(420, 365);
}
};

const getModalDimensionObject = (width: number, height: number): ModalDimension => ({ width: width, height: height });
const getModalDimensionObject = (width: number | string, height: number | string): ModalDimension => ({
width: width,
height: height
});

export const MODAL_DATA = new InjectionToken<unknown>('MODAL_DATA');

Expand Down
40 changes: 35 additions & 5 deletions projects/components/src/multi-select/multi-select.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,10 @@ import {
Output,
QueryList
} from '@angular/core';
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
import { IconType } from '@hypertrace/assets-library';
import { queryListAndChanges$ } from '@hypertrace/common';
import { queryListAndChanges$, SubscriptionLifecycle } from '@hypertrace/common';
import { isEqual } from 'lodash-es';
import { BehaviorSubject, combineLatest, EMPTY, Observable, of, Subject } from 'rxjs';
import { map } from 'rxjs/operators';
import { ButtonRole, ButtonStyle } from '../button/button';
Expand All @@ -19,11 +21,18 @@ import { SearchBoxDisplayMode } from '../search-box/search-box.component';
import { SelectOptionComponent } from '../select/select-option.component';
import { SelectSize } from '../select/select-size';
import { MultiSelectJustify } from './multi-select-justify';

@Component({
selector: 'ht-multi-select',
styleUrls: ['./multi-select.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
providers: [
SubscriptionLifecycle,
{
provide: NG_VALUE_ACCESSOR,
useExisting: MultiSelectComponent,
multi: true
}
],
template: `
<div
class="multi-select"
Expand Down Expand Up @@ -120,7 +129,7 @@ import { MultiSelectJustify } from './multi-select-justify';
</div>
`
})
export class MultiSelectComponent<V> implements AfterContentInit, OnChanges {
export class MultiSelectComponent<V> implements ControlValueAccessor, AfterContentInit, OnChanges {
@Input()
public size: SelectSize = SelectSize.Medium;

Expand Down Expand Up @@ -167,6 +176,9 @@ export class MultiSelectComponent<V> implements AfterContentInit, OnChanges {
public popoverOpen: boolean = false;
public triggerValues$: Observable<TriggerValues> = new Observable();

private propagateControlValueChange?: (value: V[] | undefined) => void;
private propagateControlValueChangeOnTouch?: (value: V[] | undefined) => void;

public ngAfterContentInit(): void {
this.allOptions$ = this.allOptionsList !== undefined ? queryListAndChanges$(this.allOptionsList) : EMPTY;
this.filteredOptions$ = combineLatest([this.allOptions$, this.searchSubject]).pipe(
Expand Down Expand Up @@ -215,24 +227,37 @@ export class MultiSelectComponent<V> implements AfterContentInit, OnChanges {
}

const selected = this.isSelectedItem(item)
? this.selected?.filter(value => value !== item.value)
? this.selected?.filter(value => !isEqual(value, item.value))
: (this.selected ?? []).concat(item.value);

this.setSelection(selected ?? []);
}

public isSelectedItem(item: SelectOptionComponent<V>): boolean {
return this.selected !== undefined && this.selected.filter(value => value === item.value).length > 0;
return this.selected !== undefined && this.selected.filter(value => isEqual(value, item.value)).length > 0;
}

public preventClickDefault(event: Event): void {
event.preventDefault();
}

public writeValue(value?: V[]): void {
this.setSelection(value ?? []);
}

public registerOnChange(onChange: (value: V[] | undefined) => void): void {
this.propagateControlValueChange = onChange;
}

public registerOnTouched(onTouch: (value: V[] | undefined) => void): void {
this.propagateControlValueChangeOnTouch = onTouch;
}

private setSelection(selected: V[]): void {
this.selected = selected;
this.setTriggerLabel();
this.selectedChange.emit(this.selected);
this.propagateValueChangeToFormControl(this.selected);
}

private setTriggerLabel(): void {
Expand All @@ -256,6 +281,11 @@ export class MultiSelectComponent<V> implements AfterContentInit, OnChanges {
})
);
}

private propagateValueChangeToFormControl(value: V[] | undefined): void {
this.propagateControlValueChange?.(value);
this.propagateControlValueChangeOnTouch?.(value);
}
}

interface TriggerValues {
Expand Down
Loading