diff --git a/libs/design/button/src/button/button.component.spec.ts b/libs/design/button/src/button/button.component.spec.ts index cc87515a1e..c87779bff9 100644 --- a/libs/design/button/src/button/button.component.spec.ts +++ b/libs/design/button/src/button/button.component.spec.ts @@ -207,17 +207,15 @@ describe('@daffodil/design/button | DaffButtonComponent', () => { }); }); - describe('using the status property of a button', () => { - it('should not set a default status', () => { - expect(component.status).toBeFalsy(); - }); + it('should take status as an input', () => { + wrapper.status = 'warn'; + fixture.detectChanges(); - it('should add the class of the defined status to the host element', () => { - wrapper.status = 'warn'; - fixture.detectChanges(); + expect(de.nativeElement.classList.contains('daff-warn')).toEqual(true); + }); - expect(de.nativeElement.classList.contains('daff-warn')).toEqual(true); - }); + it('should not set a default status', () => { + expect(component.status).toBeFalsy(); }); describe('using the tabindex property of a button', () => { diff --git a/libs/design/button/src/button/button.component.ts b/libs/design/button/src/button/button.component.ts index 719dad96a3..54bacf8eba 100644 --- a/libs/design/button/src/button/button.component.ts +++ b/libs/design/button/src/button/button.component.ts @@ -17,9 +17,8 @@ import { DaffSuffixable, daffPrefixableMixin, daffSuffixableMixin, - DaffStatusable, - daffStatusMixin, DaffArticleEncapsulatedDirective, + DaffStatusableDirective, } from '@daffodil/design'; import { DaffButtonSizableDirective } from './button-sizable.directive'; @@ -43,7 +42,7 @@ class DaffButtonBase{ constructor(public _elementRef: ElementRef, public _renderer: Renderer2) {} } -const _daffButtonBase = daffPrefixableMixin(daffSuffixableMixin(daffColorMixin(daffStatusMixin((DaffButtonBase))))); +const _daffButtonBase = daffPrefixableMixin(daffSuffixableMixin(daffColorMixin((DaffButtonBase)))); export type DaffButtonType = 'daff-button' | 'daff-stroked-button' | 'daff-raised-button' | 'daff-flat-button' | 'daff-icon-button' | 'daff-underline-button' | undefined; @@ -78,20 +77,24 @@ enum DaffButtonTypeEnum { styleUrls: ['./button.component.scss'], //todo(damienwebdev): remove once decorators hit stage 3 - https://github.com/microsoft/TypeScript/issues/7342 // eslint-disable-next-line @angular-eslint/no-inputs-metadata-property - inputs: ['color', 'status'], + inputs: ['color'], hostDirectives: [ { directive: DaffArticleEncapsulatedDirective }, { directive: DaffButtonSizableDirective, inputs: ['size'], }, + { + directive: DaffStatusableDirective, + inputs: ['status'], + }, ], encapsulation: ViewEncapsulation.None, changeDetection: ChangeDetectionStrategy.OnPush, }) export class DaffButtonComponent extends _daffButtonBase - implements OnInit, DaffPrefixable, DaffSuffixable, DaffColorable, DaffStatusable { + implements OnInit, DaffPrefixable, DaffSuffixable, DaffColorable { private buttonType: DaffButtonType; diff --git a/libs/design/notification/examples/src/notification-status/notification-status.component.html b/libs/design/notification/examples/src/notification-status/notification-status.component.html index 9e2621d55c..649aa74140 100644 --- a/libs/design/notification/examples/src/notification-status/notification-status.component.html +++ b/libs/design/notification/examples/src/notification-status/notification-status.component.html @@ -1,7 +1,7 @@ - +
Title
This is the subtitle with information
@@ -9,5 +9,5 @@ \ No newline at end of file diff --git a/libs/design/notification/src/notification-theme.scss b/libs/design/notification/src/notification-theme.scss index 96a04a4dd3..8e3b379286 100644 --- a/libs/design/notification/src/notification-theme.scss +++ b/libs/design/notification/src/notification-theme.scss @@ -37,7 +37,7 @@ } } - &.daff-error { + &.daff-danger { background: theming.daff-color(theming.$daff-red, 10); border: 1px solid theming.daff-color(theming.$daff-red, 20); color: theming.daff-text-contrast(theming.daff-color(theming.$daff-red, 10)); diff --git a/libs/design/notification/src/notification/notification.component.spec.ts b/libs/design/notification/src/notification/notification.component.spec.ts index 6fb0528700..0c8a4dd726 100644 --- a/libs/design/notification/src/notification/notification.component.spec.ts +++ b/libs/design/notification/src/notification/notification.component.spec.ts @@ -1,9 +1,6 @@ import { Component, - ContentChild, DebugElement, - Input, - ViewChild, } from '@angular/core'; import { waitForAsync, @@ -18,7 +15,6 @@ import { DaffNotificationComponent, DaffNotificationOrientation, } from './notification.component'; -import { DaffNotificationActionsDirective } from '../notification-actions/notification-actions.directive'; @Component ({ template: ` @@ -133,17 +129,11 @@ describe('@daffodil/design/notification | DaffNotificationComponent', () => { }); }); - describe('using the status property of a notification', () => { - it('should not set a default status', () => { - expect(component.status).toBeFalsy(); - }); - - it('should add the class of the defined status to the host element', () => { - wrapper.status = 'warn'; - fixture.detectChanges(); + it('should take status as an input', () => { + wrapper.status = 'warn'; + fixture.detectChanges(); - expect(de.nativeElement.classList.contains('daff-warn')).toEqual(true); - }); + expect(de.nativeElement.classList.contains('daff-warn')).toEqual(true); }); describe('setting the orientation of a notification', () => { diff --git a/libs/design/notification/src/notification/notification.component.ts b/libs/design/notification/src/notification/notification.component.ts index 2e196240f3..aa8b85174b 100644 --- a/libs/design/notification/src/notification/notification.component.ts +++ b/libs/design/notification/src/notification/notification.component.ts @@ -16,22 +16,12 @@ import { DaffArticleEncapsulatedDirective, DaffPrefixable, DaffPrefixDirective, - DaffStatusable, + DaffStatusableDirective, DaffStatusEnum, - daffStatusMixin, } from '@daffodil/design'; import { DaffNotificationActionsDirective } from '../notification-actions/notification-actions.directive'; -/** - * An _elementRef is needed for the core mixins - */ -class DaffNotificationBase { - constructor(public _elementRef: ElementRef, public _renderer: Renderer2) {} -} - -const _daffNotificationBase = daffStatusMixin(DaffNotificationBase); - export type DaffNotificationOrientation = 'horizontal' | 'vertical'; enum DaffNotificationOrientationEnum { @@ -47,18 +37,17 @@ enum DaffNotificationOrientationEnum { selector: 'daff-notification', templateUrl: './notification.component.html', styleUrls: ['./notification.component.scss'], - // todo(damienwebdev): remove once decorators hit stage 3 - https://github.com/microsoft/TypeScript/issues/7342 - // eslint-disable-next-line @angular-eslint/no-inputs-metadata-property - inputs: ['status'], - hostDirectives: [{ - directive: DaffArticleEncapsulatedDirective, - }], + hostDirectives: [ + { directive: DaffArticleEncapsulatedDirective }, + { + directive: DaffStatusableDirective, + inputs: ['status'], + }, + ], encapsulation: ViewEncapsulation.None, changeDetection: ChangeDetectionStrategy.OnPush, }) -export class DaffNotificationComponent - extends _daffNotificationBase - implements DaffPrefixable, DaffStatusable { +export class DaffNotificationComponent implements DaffPrefixable { faTimes = faTimes; @ContentChild(DaffPrefixDirective) _prefix: DaffPrefixDirective; @@ -74,7 +63,7 @@ export class DaffNotificationComponent * Sets role to status on all other instances. */ @HostBinding('attr.role') get role() { - return this.status === DaffStatusEnum.Warn || this.status === DaffStatusEnum.Danger ? 'alert' : 'status'; + return this.statusDirective.status === DaffStatusEnum.Warn || this.statusDirective.status === DaffStatusEnum.Danger ? 'alert' : 'status'; }; @HostBinding('class.vertical') get verticalOrientation() { @@ -88,6 +77,8 @@ export class DaffNotificationComponent /** Whether or not a notification is closable */ @Input() @HostBinding('class.dismissible') dismissible = false; + constructor(private statusDirective: DaffStatusableDirective) {} + private _orientation: DaffNotificationOrientation = DaffNotificationOrientationEnum.Vertical; @Input() @@ -103,13 +94,6 @@ export class DaffNotificationComponent } }; - constructor( - private elementRef: ElementRef, - private renderer: Renderer2, - ) { - super(elementRef, renderer); - } - /** * Output event triggered when the close icon is clicked. */ diff --git a/libs/design/src/core/statusable/public_api.ts b/libs/design/src/core/statusable/public_api.ts index edce10f826..7d15d0daee 100644 --- a/libs/design/src/core/statusable/public_api.ts +++ b/libs/design/src/core/statusable/public_api.ts @@ -3,4 +3,4 @@ export { DaffStatus, DaffStatusEnum, } from './statusable'; -export { daffStatusMixin } from './statusable-mixin'; +export { DaffStatusableDirective } from './statusable.directive'; diff --git a/libs/design/src/core/statusable/statusable-mixin.ts b/libs/design/src/core/statusable/statusable-mixin.ts deleted file mode 100644 index 3cda28fcf5..0000000000 --- a/libs/design/src/core/statusable/statusable-mixin.ts +++ /dev/null @@ -1,55 +0,0 @@ -import { - ElementRef, - Input, - Renderer2, -} from '@angular/core'; - -import { DaffStatus } from './statusable'; -import { Constructor } from '../constructor/constructor'; - -interface HasElementRef { - _elementRef: ElementRef; - _renderer: Renderer2; -} - -export function -daffStatusMixin>(Base: T, defaultStatus?: DaffStatus) { - class DaffStatusMixinClass extends Base { - // TODO move this back to private in Typescript 3.1 - _status: DaffStatus; - - get status(): DaffStatus { - return this._status; - } - - set status(value: DaffStatus) { - // Handles the default status - const incomingStatus = value || defaultStatus; - - if (incomingStatus === this._status) { // Only run the dom-render if a change occurs - return; - } - - // Removes the old status - if (this._status) { - this._renderer.removeClass(this._elementRef.nativeElement, `daff-${this._status}`); - } - - if (incomingStatus) { - this._renderer.addClass(this._elementRef.nativeElement, `daff-${incomingStatus}`); - } - - this._status = incomingStatus; - } - - constructor(...args: any[]) { - super(...args); - this.status = defaultStatus; - } - }; - - // TODO: ugly workaround for https://github.com/microsoft/TypeScript/issues/7342#issuecomment-624298133 - Input()(DaffStatusMixinClass.prototype, 'status'); - - return DaffStatusMixinClass; -} diff --git a/libs/design/src/core/statusable/statusable.directive.spec.ts b/libs/design/src/core/statusable/statusable.directive.spec.ts new file mode 100644 index 0000000000..b8e4c8b2ec --- /dev/null +++ b/libs/design/src/core/statusable/statusable.directive.spec.ts @@ -0,0 +1,86 @@ +import { + Component, + DebugElement, +} from '@angular/core'; +import { + waitForAsync, + ComponentFixture, + TestBed, +} from '@angular/core/testing'; +import { By } from '@angular/platform-browser'; + +import { DaffStatus } from './statusable'; +import { DaffStatusableDirective } from './statusable.directive'; + +@Component({ + template: ` +
`, +}) + +class WrapperComponent { + status: DaffStatus; +} + +describe('@daffodil/design | DaffStatusableDirective', () => { + let wrapper: WrapperComponent; + let de: DebugElement; + let fixture: ComponentFixture; + let directive: DaffStatusableDirective; + + beforeEach(waitForAsync(() => { + TestBed.configureTestingModule({ + declarations: [ + WrapperComponent, + ], + imports: [ + DaffStatusableDirective, + ], + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(WrapperComponent); + wrapper = fixture.componentInstance; + de = fixture.debugElement.query(By.css('[daffStatusable]')); + + directive = de.injector.get(DaffStatusableDirective); + fixture.detectChanges(); + }); + + it('should create', () => { + expect(wrapper).toBeTruthy(); + expect(directive).toBeTruthy(); + }); + + it('should take status as an input', () => { + expect(directive.status).toEqual(wrapper.status); + }); + + it('should add a class of .daff-warn to the host element if status is set to warn', () => { + wrapper.status = 'warn'; + fixture.detectChanges(); + + expect(directive.class).toEqual(jasmine.objectContaining({ + 'daff-warn': true, + })); + }); + + it('should add a class of .daff-danger to the host element if status is set to danger', () => { + wrapper.status = 'danger'; + fixture.detectChanges(); + + expect(directive.class).toEqual(jasmine.objectContaining({ + 'daff-danger': true, + })); + }); + + it('should add a class of .daff-success to the host element if status is set to success', () => { + wrapper.status = 'success'; + fixture.detectChanges(); + + expect(directive.class).toEqual(jasmine.objectContaining({ + 'daff-success': true, + })); + }); +}); diff --git a/libs/design/src/core/statusable/statusable.directive.ts b/libs/design/src/core/statusable/statusable.directive.ts new file mode 100644 index 0000000000..404bfeaa0e --- /dev/null +++ b/libs/design/src/core/statusable/statusable.directive.ts @@ -0,0 +1,55 @@ +import { + Directive, + HostBinding, + Input, +} from '@angular/core'; + +import { + DaffStatus, + DaffStatusEnum, + DaffStatusable, +} from './statusable'; + +/** + * The `DaffStatusableDirective` allows a component to conditionally apply status-specific + * styles by setting CSS classes based on the specified status. This directive is useful + * for indicating different statuses such as warnings, errors, or success states. + * + * ## Example + * + * ```html + *
Status content
+ * ``` + * + * ## Styles + * + * The directive applies the following CSS classes based on the status: + * + * - `daff-warn`: Applied when the status is `Warn`. + * - `daff-danger`: Applied when the status is `Danger`. + * - `daff-success`: Applied when the status is `Success`. + */ +@Directive({ + selector: '[daffStatusable]', + standalone: true, +}) +export class DaffStatusableDirective implements DaffStatusable { + + /** + * Dynamically sets the CSS classes based on the status. + * @docs-private + */ + @HostBinding('class') get class() { + return { + 'daff-warn': this.status === DaffStatusEnum.Warn, + 'daff-danger': this.status === DaffStatusEnum.Danger, + 'daff-success': this.status === DaffStatusEnum.Success, + }; + } + + /** + * Sets the status on a component. + */ + @Input() status: DaffStatus; +} + diff --git a/libs/design/src/core/statusable/statusable.spec.ts b/libs/design/src/core/statusable/statusable.spec.ts deleted file mode 100644 index 3a48bf56df..0000000000 --- a/libs/design/src/core/statusable/statusable.spec.ts +++ /dev/null @@ -1,106 +0,0 @@ -import { ElementRef } from '@angular/core'; - -import { daffStatusMixin } from './statusable-mixin'; - -class TestingClass { - element: HTMLElement = document.createElement('div'); - - _elementRef = new ElementRef(this.element); - _renderer: any = { - addClass: (el: HTMLElement, className: string) => { - el.classList.add(className); - }, - removeClass: (el: HTMLElement, className: string) => { - el.classList.remove(className); - }, - }; -} - -describe('@daffodil/design | daffStatusMixin', () => { - let instance; - let classWithStatus; - - beforeEach(() => { - classWithStatus = daffStatusMixin(TestingClass); - instance = new classWithStatus(); - }); - - it('should add a status property to an existing class', () => { - expect('status' in instance).toBeTruthy(); - }); - - it('should allow the consuming class to optionally define a default status', () => { - classWithStatus = daffStatusMixin(TestingClass, 'warn'); - instance = new classWithStatus(); - - expect(instance.status).toEqual('warn'); - expect(instance.element.classList).toContain('daff-warn'); - }); - - describe('when a status is specified', () => { - - it('should set a namespaced status class', () => { - instance.status = 'warn'; - - expect(instance.element.classList).toContain('daff-warn'); - }); - }); - - describe('when a status is not specified', () => { - - it('should default to no status class', () => { - instance.status = undefined; - expect(instance.element.classList.length).toEqual(0); - }); - }); - - describe('when `status` changes', () => { - - beforeEach(() => { - instance.status = 'warn'; - instance.status = 'danger'; - }); - - it('should add the new status class', () => { - expect(instance.element.classList).toContain('daff-danger'); - }); - - it('should remove the provious status class', () => { - expect(instance.element.classList).not.toContain('daff-warn'); - }); - }); - - describe('when a default status is undefined', () => { - describe('and status is set to null or undefined', () => { - it('should do nothing', () => { - instance.status = null; - expect(instance.element.classList.value).toEqual(''); - - instance.status = undefined; - expect(instance.element.classList.value).toEqual(''); - }); - }); - }); - - describe('when a default status is specified', () => { - - beforeEach(() => { - classWithStatus = daffStatusMixin(TestingClass, 'warn'); - instance = new classWithStatus(); - }); - - describe('and status is set to null or undefined', () => { - it('should set status to the default status ', () => { - instance.status = null; - - expect(instance.status).toEqual('warn'); - expect(instance.element.classList).toContain('daff-warn'); - - instance.status = undefined; - - expect(instance.status).toEqual('warn'); - expect(instance.element.classList).toContain('daff-warn'); - }); - }); - }); -}); diff --git a/libs/design/src/core/statusable/statusable.ts b/libs/design/src/core/statusable/statusable.ts index b29c4a5349..49495bc6cf 100644 --- a/libs/design/src/core/statusable/statusable.ts +++ b/libs/design/src/core/statusable/statusable.ts @@ -1,9 +1,25 @@ +/** + * The `DaffStatusable` interface defines a component that can have a status. + * This status determines the styling or behavior of the component. + */ export interface DaffStatusable { + /** + * The status of the component. + */ status: DaffStatus; } +/** + * The `DaffStatus` type defines the possible status values that a component can have. + * - 'warn': Indicates a warning status. + * - 'danger': Indicates a danger or error status. + * - 'success': Indicates a success status. + */ export type DaffStatus = 'warn' | 'danger' | 'success'; +/** + * The `DaffStatusEnum` enumerates the possible status values for a component. + */ export enum DaffStatusEnum { Warn = 'warn', Danger = 'danger', diff --git a/libs/design/toast/src/toast/toast.component.spec.ts b/libs/design/toast/src/toast/toast.component.spec.ts index 5296a100b5..381574aa64 100644 --- a/libs/design/toast/src/toast/toast.component.spec.ts +++ b/libs/design/toast/src/toast/toast.component.spec.ts @@ -10,10 +10,7 @@ import { import { By } from '@angular/platform-browser'; import { of } from 'rxjs'; -import { - DaffStatus, - DaffStatusEnum, -} from '@daffodil/design'; +import { DaffStatus } from '@daffodil/design'; import { DaffToastComponent } from './toast.component'; import { DaffToast } from '../interfaces/toast'; @@ -73,17 +70,11 @@ describe('DaffToastComponent', () => { }); }); - describe('using the status property of a toast', () => { - it('should not set a default status', () => { - expect(component.status).toBeFalsy(); - }); - - it('should add the class of the defined status to the host element', () => { - wrapper.status = DaffStatusEnum.Warn; - fixture.detectChanges(); + it('should take status as an input', () => { + wrapper.status = 'warn'; + fixture.detectChanges(); - expect(de.nativeElement.classList.contains('daff-warn')).toEqual(true); - }); + expect(de.nativeElement.classList.contains('daff-warn')).toEqual(true); }); it('should have a role of status', () => { diff --git a/libs/design/toast/src/toast/toast.component.ts b/libs/design/toast/src/toast/toast.component.ts index c2977f957b..cc45bde4e6 100644 --- a/libs/design/toast/src/toast/toast.component.ts +++ b/libs/design/toast/src/toast/toast.component.ts @@ -5,7 +5,6 @@ import { import { Component, ElementRef, - Renderer2, HostBinding, ContentChild, ViewEncapsulation, @@ -22,23 +21,13 @@ import { DaffFocusStackService, DaffPrefixable, DaffPrefixDirective, - DaffStatusable, - daffStatusMixin, + DaffStatusableDirective, } from '@daffodil/design'; import { DaffToast } from '../interfaces/toast'; import { daffToastChangesFocus } from '../service/changes-focus'; import { DaffToastActionsDirective } from '../toast-actions/toast-actions.directive'; -/** - * An _elementRef is needed for the core mixins - */ -class DaffToastBase { - constructor(public _elementRef: ElementRef, public _renderer: Renderer2) {} -} - -const _daffToastBase = daffStatusMixin(DaffToastBase); - /** * DaffToastComponent provides a way to display and * communicate information for user actions or system updates. @@ -47,18 +36,17 @@ const _daffToastBase = daffStatusMixin(DaffToastBase); selector: 'daff-toast', templateUrl: './toast.component.html', styleUrls: ['./toast.component.scss'], - // todo(damienwebdev): remove once decorators hit stage 3 - https://github.com/microsoft/TypeScript/issues/7342 - // eslint-disable-next-line @angular-eslint/no-inputs-metadata-property - inputs: ['status'], - hostDirectives: [{ - directive: DaffArticleEncapsulatedDirective, - }], + hostDirectives: [ + { directive: DaffArticleEncapsulatedDirective }, + { + directive: DaffStatusableDirective, + inputs: ['status'], + }, + ], encapsulation: ViewEncapsulation.None, changeDetection: ChangeDetectionStrategy.OnPush, }) -export class DaffToastComponent - extends _daffToastBase - implements DaffPrefixable, DaffStatusable, AfterContentInit, AfterViewInit, OnDestroy { +export class DaffToastComponent implements DaffPrefixable, AfterContentInit, AfterViewInit, OnDestroy { /** @docs-private */ @HostBinding('class.daff-toast') class = true; @@ -84,12 +72,10 @@ export class DaffToastComponent private _focusTrap: ConfigurableFocusTrap; constructor( - private elementRef: ElementRef, - private renderer: Renderer2, + private _elementRef: ElementRef, private _focusTrapFactory: ConfigurableFocusTrapFactory, private _focusStack: DaffFocusStackService, ) { - super(elementRef, renderer); } ngAfterContentInit() {