diff --git a/apps/demo/src/app/routing/indicator/indicator.component.html b/apps/demo/src/app/routing/indicator/indicator.component.html index 0460147c49..7cb621aec0 100644 --- a/apps/demo/src/app/routing/indicator/indicator.component.html +++ b/apps/demo/src/app/routing/indicator/indicator.component.html @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/apps/demo/src/app/routing/routing-component.module.ts b/apps/demo/src/app/routing/routing-component.module.ts index 2cf92ffe3e..2ae8de98df 100644 --- a/apps/demo/src/app/routing/routing-component.module.ts +++ b/apps/demo/src/app/routing/routing-component.module.ts @@ -1,7 +1,7 @@ import { CommonModule } from '@angular/common'; import { NgModule } from '@angular/core'; -import { DaffProgressIndicatorModule } from '@daffodil/design'; +import { DaffProgressBarModule } from '@daffodil/design/progress-bar'; import { DemoIndicatorComponent } from './indicator/indicator.component'; @@ -9,7 +9,7 @@ import { DemoIndicatorComponent } from './indicator/indicator.component'; @NgModule({ imports: [ CommonModule, - DaffProgressIndicatorModule, + DaffProgressBarModule, ], declarations: [ DemoIndicatorComponent, diff --git a/apps/design-land/src/app/app-routing.module.ts b/apps/design-land/src/app/app-routing.module.ts index 1b7483b21f..c8fbd33238 100644 --- a/apps/design-land/src/app/app-routing.module.ts +++ b/apps/design-land/src/app/app-routing.module.ts @@ -35,6 +35,7 @@ export const appRoutes: Routes = [ { path: 'modal', loadChildren: () => import('./modal/modal.module').then(m => m.DesignLandModalModule) }, { path: 'notification', loadChildren: () => import('./notification/notification.module').then(m => m.DesignLandNotificationModule) }, { path: 'paginator', loadChildren: () => import('./paginator/paginator.module').then(m => m.DesignLandPaginatorModule) }, + { path: 'progress-bar', loadChildren: () => import('./progress-bar/progress-bar.module').then(m => m.DesignLandProgressBarModule) }, { path: 'progress-indicator', loadChildren: () => import('./progress-indicator/progress-indicator.module').then(m => m.DesignLandProgressIndicatorModule) }, { path: 'qty-dropdown', loadChildren: () => import('./qty-dropdown/qty-dropdown.module').then(m => m.DesignLandQtyDropdownModule) }, { path: 'quantity-field', loadChildren: () => import('./quantity-field/quantity-field.module').then(m => m.DesignLandQuantityFieldModule) }, diff --git a/apps/design-land/src/app/app.component.ts b/apps/design-land/src/app/app.component.ts index 1e78359623..cac2db787e 100644 --- a/apps/design-land/src/app/app.component.ts +++ b/apps/design-land/src/app/app.component.ts @@ -22,6 +22,7 @@ import { MODAL_EXAMPLES } from '@daffodil/design/modal/examples'; import { NAVBAR_EXAMPLES } from '@daffodil/design/navbar/examples'; import { NOTIFICATION_EXAMPLES } from '@daffodil/design/notification/examples'; import { PAGINATOR_EXAMPLES } from '@daffodil/design/paginator/examples'; +import { PROGRESS_BAR_EXAMPLES } from '@daffodil/design/progress-bar/examples'; import { QUANTITY_FIELD_EXAMPLES } from '@daffodil/design/quantity-field/examples'; import { RADIO_EXAMPLES } from '@daffodil/design/radio/examples'; import { SIDEBAR_EXAMPLES } from '@daffodil/design/sidebar/examples'; @@ -59,6 +60,7 @@ export class DesignLandAppComponent { ...QUANTITY_FIELD_EXAMPLES, ...LIST_EXAMPLES, ...PAGINATOR_EXAMPLES, + ...PROGRESS_BAR_EXAMPLES, ...IMAGE_EXAMPLES, ...INPUT_EXAMPLES, ...SIDEBAR_EXAMPLES, diff --git a/apps/design-land/src/app/progress-bar/progress-bar-routing.module.ts b/apps/design-land/src/app/progress-bar/progress-bar-routing.module.ts new file mode 100644 index 0000000000..49cf3292fa --- /dev/null +++ b/apps/design-land/src/app/progress-bar/progress-bar-routing.module.ts @@ -0,0 +1,21 @@ +import { NgModule } from '@angular/core'; +import { + Routes, + RouterModule, +} from '@angular/router'; + +import { DesignLandProgressBarComponent } from './progress-bar.component'; + +export const progressBarRoutes: Routes = [ + { path: '', component: DesignLandProgressBarComponent }, +]; + +@NgModule({ + imports: [ + RouterModule.forChild(progressBarRoutes), + ], + exports: [ + RouterModule, + ], +}) +export class DesignLandProgressBarRoutingModule {} diff --git a/apps/design-land/src/app/progress-bar/progress-bar.component.html b/apps/design-land/src/app/progress-bar/progress-bar.component.html new file mode 100644 index 0000000000..5a4b69f57c --- /dev/null +++ b/apps/design-land/src/app/progress-bar/progress-bar.component.html @@ -0,0 +1,26 @@ + +

Progress Bar

+
A progress bar provides visual feedback about the duration or progress of a task or operation.
+ +

Types

+

There are two types of progress bars: determinate and indeterminate. They are determinate by default.

+ +

Determinate

+

Determinate progress bars should be used when the loading percentage of a task or operation is known.

+ + +

Indeterminate

+

Indeterminate progress bars should be used when the loading percentage of a task or operation is unknown or cannot be calculated.

+ + +

Theming

+

The progress bar color is defined by using the color property. By default, the color is set to primary. This can be changed to one of the supported colors.

+

Supported colors: primary | secondary | tertiary | theme | theme-contrast | white | black

+ +
theme, theme-contrast, white, and black should be used with caution to ensure that there is sufficient contrast.
+ + + +

Accessibility

+ The progress bar component works with the ARIA role="progressbar" to provide an accessible experience. A label should always be provided by using label[daffFormLabel], aria-label, or aria-labelledby. +
diff --git a/apps/design-land/src/app/progress-bar/progress-bar.component.spec.ts b/apps/design-land/src/app/progress-bar/progress-bar.component.spec.ts new file mode 100644 index 0000000000..59fb733475 --- /dev/null +++ b/apps/design-land/src/app/progress-bar/progress-bar.component.spec.ts @@ -0,0 +1,29 @@ +import { + waitForAsync, + ComponentFixture, + TestBed, +} from '@angular/core/testing'; + +import { DesignLandProgressBarComponent } from './progress-bar.component'; + +describe('DesignLandProgressBarComponent', () => { + let component: DesignLandProgressBarComponent; + let fixture: ComponentFixture; + + beforeEach(waitForAsync(() => { + TestBed.configureTestingModule({ + declarations: [ DesignLandProgressBarComponent ], + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(DesignLandProgressBarComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/apps/design-land/src/app/progress-bar/progress-bar.component.ts b/apps/design-land/src/app/progress-bar/progress-bar.component.ts new file mode 100644 index 0000000000..8c286fa3cd --- /dev/null +++ b/apps/design-land/src/app/progress-bar/progress-bar.component.ts @@ -0,0 +1,7 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'design-land-progress-bar', + templateUrl: './progress-bar.component.html', +}) +export class DesignLandProgressBarComponent { } diff --git a/apps/design-land/src/app/progress-bar/progress-bar.module.ts b/apps/design-land/src/app/progress-bar/progress-bar.module.ts new file mode 100644 index 0000000000..652df6623d --- /dev/null +++ b/apps/design-land/src/app/progress-bar/progress-bar.module.ts @@ -0,0 +1,24 @@ +import { CommonModule } from '@angular/common'; +import { NgModule } from '@angular/core'; + +import { DaffArticleModule } from '@daffodil/design/article'; +import { DaffProgressBarModule } from '@daffodil/design/progress-bar'; + +import { DesignLandProgressBarRoutingModule } from './progress-bar-routing.module'; +import { DesignLandProgressBarComponent } from './progress-bar.component'; +import { DesignLandExampleViewerModule } from '../core/code-preview/container/example-viewer.module'; + +@NgModule({ + declarations: [ + DesignLandProgressBarComponent, + ], + imports: [ + CommonModule, + DesignLandProgressBarRoutingModule, + DesignLandExampleViewerModule, + + DaffProgressBarModule, + DaffArticleModule, + ], +}) +export class DesignLandProgressBarModule { } diff --git a/apps/design-land/src/app/progress-indicator/progress-indicator.module.ts b/apps/design-land/src/app/progress-indicator/progress-indicator.module.ts index b5210abb9c..e3c7862751 100644 --- a/apps/design-land/src/app/progress-indicator/progress-indicator.module.ts +++ b/apps/design-land/src/app/progress-indicator/progress-indicator.module.ts @@ -5,6 +5,7 @@ import { DaffProgressIndicatorModule } from '@daffodil/design'; import { DesignLandProgressIndicatorRoutingModule } from './progress-indicator-routing.module'; import { DesignLandProgressIndicatorComponent } from './progress-indicator.component'; +import { DesignLandExampleViewerModule } from '../core/code-preview/container/example-viewer.module'; @NgModule({ declarations: [ @@ -14,6 +15,7 @@ import { DesignLandProgressIndicatorComponent } from './progress-indicator.compo CommonModule, DaffProgressIndicatorModule, DesignLandProgressIndicatorRoutingModule, + DesignLandExampleViewerModule, ], }) export class DesignLandProgressIndicatorModule { } diff --git a/apps/design-land/src/assets/nav.json b/apps/design-land/src/assets/nav.json index fa5199feed..ee1a4d73f5 100644 --- a/apps/design-land/src/assets/nav.json +++ b/apps/design-land/src/assets/nav.json @@ -81,7 +81,14 @@ "data": {} }, { - "title": "Progress Indicator", + "title": "Progress Bar", + "url": "progress-bar", + "id": "progress-bar", + "items": [], + "data": {} + }, + { + "title": "Progress Indicator (Deprecated)", "url": "progress-indicator", "id": "progress-indicator", "items": [], diff --git a/libs/design/progress-bar/README.md b/libs/design/progress-bar/README.md new file mode 100644 index 0000000000..c4b3cab530 --- /dev/null +++ b/libs/design/progress-bar/README.md @@ -0,0 +1,27 @@ +# Progress Bar +A progress bar provides visual feedback about the duration or progress of a task or operation. + +## Types +There are two types of progress bars: `determinate` and `indeterminate`. They are `determinate` by default. + +### Determinate +Determinate progress bars should be used when the percentage of a task or operation is known. + + + +### Indeterminate +Indeterminate progress bars should be used when the loading percentage of a task or operation is unknown or cannot be calculated. + + + +## Theming +The progress bar color is defined by using the `color` property. By default, the color is set to `primary`. This can be changed to one of the supported colors. + +Supported colors: `primary | secondary | tertiary | theme | theme-contrast | white | black` + +> `theme`, `theme-contrast`, `white`, and `black` should be used with caution to ensure that there is sufficient contrast. + + + +## Accessibility +The progress bar component works with the ARIA `role="progressbar"` to provide an accessible experience. A Label should always be provided by using `label[daffFormLabel]`, `aria-label`, or `aria-labelledby`. diff --git a/libs/design/progress-bar/examples/ng-package.json b/libs/design/progress-bar/examples/ng-package.json new file mode 100644 index 0000000000..e0bd308d7b --- /dev/null +++ b/libs/design/progress-bar/examples/ng-package.json @@ -0,0 +1,7 @@ +{ + "$schema": "../../../../node_modules/ng-packagr/ng-entrypoint.schema.json", + "lib": { + "entryFile": "src/index.ts", + "styleIncludePaths": ["../../scss"] + } +} \ No newline at end of file diff --git a/libs/design/progress-bar/examples/src/index.ts b/libs/design/progress-bar/examples/src/index.ts new file mode 100644 index 0000000000..4aaf8f92ed --- /dev/null +++ b/libs/design/progress-bar/examples/src/index.ts @@ -0,0 +1 @@ +export * from './public_api'; diff --git a/libs/design/progress-bar/examples/src/progress-bar-default/progress-bar-default.component.html b/libs/design/progress-bar/examples/src/progress-bar-default/progress-bar-default.component.html new file mode 100644 index 0000000000..bffe94bc3a --- /dev/null +++ b/libs/design/progress-bar/examples/src/progress-bar-default/progress-bar-default.component.html @@ -0,0 +1,3 @@ + + + diff --git a/libs/design/progress-bar/examples/src/progress-bar-default/progress-bar-default.component.ts b/libs/design/progress-bar/examples/src/progress-bar-default/progress-bar-default.component.ts new file mode 100644 index 0000000000..79cb0cdad2 --- /dev/null +++ b/libs/design/progress-bar/examples/src/progress-bar-default/progress-bar-default.component.ts @@ -0,0 +1,18 @@ +import { + ChangeDetectionStrategy, + Component, +} from '@angular/core'; + +@Component({ + // eslint-disable-next-line @angular-eslint/component-selector + selector: 'progress-bar-default', + templateUrl: './progress-bar-default.component.html', + styles: [` + :host { + display: flex; + flex-direction: column; + gap: 8px; + }`], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class ProgressBarDefaultComponent {} diff --git a/libs/design/progress-bar/examples/src/progress-bar-default/progress-bar-default.module.ts b/libs/design/progress-bar/examples/src/progress-bar-default/progress-bar-default.module.ts new file mode 100644 index 0000000000..6723093fd0 --- /dev/null +++ b/libs/design/progress-bar/examples/src/progress-bar-default/progress-bar-default.module.ts @@ -0,0 +1,18 @@ +import { NgModule } from '@angular/core'; + +import { DaffProgressBarModule } from '@daffodil/design/progress-bar'; + +import { ProgressBarDefaultComponent } from './progress-bar-default.component'; + +@NgModule({ + declarations: [ + ProgressBarDefaultComponent, + ], + exports: [ + ProgressBarDefaultComponent, + ], + imports: [ + DaffProgressBarModule, + ], +}) +export class ProgressBarDefaultComponentModule { } diff --git a/libs/design/progress-bar/examples/src/progress-bar-indeterminate/progress-bar-indeterminate.component.html b/libs/design/progress-bar/examples/src/progress-bar-indeterminate/progress-bar-indeterminate.component.html new file mode 100644 index 0000000000..a1ba4bae3b --- /dev/null +++ b/libs/design/progress-bar/examples/src/progress-bar-indeterminate/progress-bar-indeterminate.component.html @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/libs/design/progress-bar/examples/src/progress-bar-indeterminate/progress-bar-indeterminate.component.ts b/libs/design/progress-bar/examples/src/progress-bar-indeterminate/progress-bar-indeterminate.component.ts new file mode 100644 index 0000000000..84af206431 --- /dev/null +++ b/libs/design/progress-bar/examples/src/progress-bar-indeterminate/progress-bar-indeterminate.component.ts @@ -0,0 +1,18 @@ +import { + ChangeDetectionStrategy, + Component, +} from '@angular/core'; + +@Component({ + // eslint-disable-next-line @angular-eslint/component-selector + selector: 'progress-bar-indeterminate', + templateUrl: './progress-bar-indeterminate.component.html', + styles: [` + :host { + display: flex; + flex-direction: column; + gap: 8px; + }`], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class ProgressBarIndeterminateComponent {} diff --git a/libs/design/progress-bar/examples/src/progress-bar-indeterminate/progress-bar-indeterminate.module.ts b/libs/design/progress-bar/examples/src/progress-bar-indeterminate/progress-bar-indeterminate.module.ts new file mode 100644 index 0000000000..a2094e9839 --- /dev/null +++ b/libs/design/progress-bar/examples/src/progress-bar-indeterminate/progress-bar-indeterminate.module.ts @@ -0,0 +1,18 @@ +import { NgModule } from '@angular/core'; + +import { DaffProgressBarModule } from '@daffodil/design/progress-bar'; + +import { ProgressBarIndeterminateComponent } from './progress-bar-indeterminate.component'; + +@NgModule({ + declarations: [ + ProgressBarIndeterminateComponent, + ], + exports: [ + ProgressBarIndeterminateComponent, + ], + imports: [ + DaffProgressBarModule, + ], +}) +export class ProgressBarIndeterminateComponentModule { } diff --git a/libs/design/progress-bar/examples/src/progress-bar-themes/progress-bar-themes.component.html b/libs/design/progress-bar/examples/src/progress-bar-themes/progress-bar-themes.component.html new file mode 100644 index 0000000000..9fa7a2e773 --- /dev/null +++ b/libs/design/progress-bar/examples/src/progress-bar-themes/progress-bar-themes.component.html @@ -0,0 +1,14 @@ + + + + + \ No newline at end of file diff --git a/libs/design/progress-bar/examples/src/progress-bar-themes/progress-bar-themes.component.ts b/libs/design/progress-bar/examples/src/progress-bar-themes/progress-bar-themes.component.ts new file mode 100644 index 0000000000..dcab7ea021 --- /dev/null +++ b/libs/design/progress-bar/examples/src/progress-bar-themes/progress-bar-themes.component.ts @@ -0,0 +1,26 @@ +import { + ChangeDetectionStrategy, + Component, +} from '@angular/core'; +import { FormControl } from '@angular/forms'; + +import { DaffPalette } from '@daffodil/design'; + +@Component({ + // eslint-disable-next-line @angular-eslint/component-selector + selector: 'progress-bar-themes', + templateUrl: './progress-bar-themes.component.html', + styles: [` + :host { + display: flex; + flex-direction: column; + align-items: flex-start; + gap: 16px; + }`], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class ProgressBarThemesComponent { + color: DaffPalette = 'primary'; + + colorControl: FormControl = new FormControl(''); +} diff --git a/libs/design/progress-bar/examples/src/progress-bar-themes/progress-bar-themes.module.ts b/libs/design/progress-bar/examples/src/progress-bar-themes/progress-bar-themes.module.ts new file mode 100644 index 0000000000..940b688981 --- /dev/null +++ b/libs/design/progress-bar/examples/src/progress-bar-themes/progress-bar-themes.module.ts @@ -0,0 +1,20 @@ +import { NgModule } from '@angular/core'; +import { ReactiveFormsModule } from '@angular/forms'; + +import { DaffProgressBarModule } from '@daffodil/design/progress-bar'; + +import { ProgressBarThemesComponent } from './progress-bar-themes.component'; + +@NgModule({ + declarations: [ + ProgressBarThemesComponent, + ], + exports: [ + ProgressBarThemesComponent, + ], + imports: [ + DaffProgressBarModule, + ReactiveFormsModule, + ], +}) +export class ProgressBarThemesComponentModule { } diff --git a/libs/design/progress-bar/examples/src/public_api.ts b/libs/design/progress-bar/examples/src/public_api.ts new file mode 100644 index 0000000000..654297441b --- /dev/null +++ b/libs/design/progress-bar/examples/src/public_api.ts @@ -0,0 +1,14 @@ +import { ComponentExample } from '@daffodil/design'; + +import { ProgressBarDefaultComponent } from './progress-bar-default/progress-bar-default.component'; +import { ProgressBarDefaultComponentModule } from './progress-bar-default/progress-bar-default.module'; +import { ProgressBarIndeterminateComponent } from './progress-bar-indeterminate/progress-bar-indeterminate.component'; +import { ProgressBarIndeterminateComponentModule } from './progress-bar-indeterminate/progress-bar-indeterminate.module'; +import { ProgressBarThemesComponent } from './progress-bar-themes/progress-bar-themes.component'; +import { ProgressBarThemesComponentModule } from './progress-bar-themes/progress-bar-themes.module'; + +export const PROGRESS_BAR_EXAMPLES: ComponentExample[] = [ + { component: ProgressBarThemesComponent, module: ProgressBarThemesComponentModule }, + { component: ProgressBarIndeterminateComponent, module: ProgressBarIndeterminateComponentModule }, + { component: ProgressBarDefaultComponent, module: ProgressBarDefaultComponentModule }, +]; diff --git a/libs/design/progress-bar/ng-package.json b/libs/design/progress-bar/ng-package.json new file mode 100644 index 0000000000..b8e0b88f60 --- /dev/null +++ b/libs/design/progress-bar/ng-package.json @@ -0,0 +1,7 @@ +{ + "$schema": "../../../node_modules/ng-packagr/ng-entrypoint.schema.json", + "lib": { + "entryFile": "src/public_api.ts", + "styleIncludePaths": ["../scss"] + } +} \ No newline at end of file diff --git a/libs/design/progress-bar/src/animation/progress-bar-animation.ts b/libs/design/progress-bar/src/animation/progress-bar-animation.ts new file mode 100644 index 0000000000..72e13b0f7a --- /dev/null +++ b/libs/design/progress-bar/src/animation/progress-bar-animation.ts @@ -0,0 +1,18 @@ +import { + animate, + state, + style, + transition, + trigger, + AnimationTriggerMetadata, +} from '@angular/animations'; + +export const daffProgressBarAnimation: { + readonly fill: AnimationTriggerMetadata; +} = { + fill: trigger('fill', [ + state('*', style({ transform: 'scaleX(calc({{ percentage }}/100))' }), { params: { percentage: 0 }}), + transition('void <=> *', animate(0)), + transition('* <=> *', animate(1000)), + ]), +}; diff --git a/libs/design/progress-bar/src/index.ts b/libs/design/progress-bar/src/index.ts new file mode 100644 index 0000000000..4aaf8f92ed --- /dev/null +++ b/libs/design/progress-bar/src/index.ts @@ -0,0 +1 @@ +export * from './public_api'; diff --git a/libs/design/progress-bar/src/progress-bar-label/progress-bar-label.directive.spec.ts b/libs/design/progress-bar/src/progress-bar-label/progress-bar-label.directive.spec.ts new file mode 100644 index 0000000000..3360c9a3a8 --- /dev/null +++ b/libs/design/progress-bar/src/progress-bar-label/progress-bar-label.directive.spec.ts @@ -0,0 +1,53 @@ +import { + Component, + DebugElement, +} from '@angular/core'; +import { + waitForAsync, + ComponentFixture, + TestBed, +} from '@angular/core/testing'; +import { By } from '@angular/platform-browser'; + +import { DaffProgressBarLabelDirective } from './progress-bar-label.directive'; + +@Component({ + template: ``, +}) + +class WrapperComponent {} + +describe('DaffProgressBarLabelDirective', () => { + let wrapper: WrapperComponent; + let de: DebugElement; + let fixture: ComponentFixture; + + beforeEach(waitForAsync(() => { + TestBed.configureTestingModule({ + declarations: [ + DaffProgressBarLabelDirective, + WrapperComponent, + ], + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(WrapperComponent); + wrapper = fixture.componentInstance; + de = fixture.debugElement.query(By.css('[daffProgressBarLabel]')); + fixture.detectChanges(); + }); + + it('should create', () => { + expect(wrapper).toBeTruthy(); + }); + + describe('[daffProgressBarLabel]', () => { + it('should add a class of "daff-progress-bar__label" to the host element', () => { + expect(de.classes).toEqual(jasmine.objectContaining({ + 'daff-progress-bar__label': true, + })); + }); + }); +}); diff --git a/libs/design/progress-bar/src/progress-bar-label/progress-bar-label.directive.ts b/libs/design/progress-bar/src/progress-bar-label/progress-bar-label.directive.ts new file mode 100644 index 0000000000..1f65cae6c3 --- /dev/null +++ b/libs/design/progress-bar/src/progress-bar-label/progress-bar-label.directive.ts @@ -0,0 +1,11 @@ +import { + Directive, + HostBinding, +} from '@angular/core'; + +@Directive({ + selector: '[daffProgressBarLabel]', +}) +export class DaffProgressBarLabelDirective { + @HostBinding('class.daff-progress-bar__label') class = true; +} diff --git a/libs/design/progress-bar/src/progress-bar-theme.scss b/libs/design/progress-bar/src/progress-bar-theme.scss new file mode 100644 index 0000000000..3a024b2f43 --- /dev/null +++ b/libs/design/progress-bar/src/progress-bar-theme.scss @@ -0,0 +1,68 @@ +@use 'sass:map'; +@use '../../scss/core'; +@use '../../scss/theming'; + +@mixin daff-progress-bar-theme($theme) { + $primary: map.get($theme, primary); + $secondary: map.get($theme, secondary); + $tertiary: map.get($theme, tertiary); + $base: core.daff-map-deep-get($theme, 'core.base'); + $base-contrast: core.daff-map-deep-get($theme, 'core.base-contrast'); + $white: core.daff-map-deep-get($theme, 'core.white'); + $black: core.daff-map-deep-get($theme, 'core.black'); + $neutral: core.daff-map-deep-get($theme, 'core.neutral'); + + .daff-progress-bar { + $root: '.daff-progress-bar'; + + #{$root}__label { + color: theming.daff-illuminate($base-contrast, $neutral, 2); + } + + #{$root}__track { + background: theming.daff-illuminate($base, $neutral, 2); + } + + &.daff-primary { + #{$root}__fill { + background: theming.daff-illuminate($base, $primary, 6); + } + } + + &.daff-secondary { + #{$root}__fill { + background: theming.daff-illuminate($base, $secondary, 6); + } + } + + &.daff-tertiary { + #{$root}__fill { + background: theming.daff-illuminate($base, $tertiary, 6); + } + } + + &.daff-theme { + #{$root}__fill { + background: $base; + } + } + + &.daff-theme-contrast { + #{$root}__fill { + background: $base-contrast; + } + } + + &.daff-white { + #{$root}__fill { + background: $white; + } + } + + &.daff-black { + #{$root}__fill { + background: $black; + } + } + } +} \ No newline at end of file diff --git a/libs/design/progress-bar/src/progress-bar.component.html b/libs/design/progress-bar/src/progress-bar.component.html new file mode 100644 index 0000000000..f1fc150238 --- /dev/null +++ b/libs/design/progress-bar/src/progress-bar.component.html @@ -0,0 +1,5 @@ + +
+
+
+
\ No newline at end of file diff --git a/libs/design/progress-bar/src/progress-bar.component.scss b/libs/design/progress-bar/src/progress-bar.component.scss new file mode 100644 index 0000000000..2e03c0695c --- /dev/null +++ b/libs/design/progress-bar/src/progress-bar.component.scss @@ -0,0 +1,53 @@ +$height: 4px; + +:host(.daff-progress-bar) { + $root: '.daff-progress-bar'; + display: flex; + flex-direction: column; + align-items: flex-start; + gap: 8px; + width: 100%; + + ::ng-deep { + #{$root}__label { + font-size: 0.875rem; + line-height: 1rem; + } + } + + #{$root}__track { + height: $height; + position: relative; + width: 100%; + } + + #{$root}__fill { + height: $height; + width: 100%; + transform: scaleX(0); + transform-origin: 0 center; + } + + &.indeterminate { + overflow: hidden; + + .indeterminate-bar { + height: 100%; + width: 100%; + animation: indeterminate-animation 1500ms infinite linear; + transform-origin: 0% 50%; + } + } +} + +@keyframes indeterminate-animation { + 0% { + transform: translateX(0) scaleX(0); + } + 20% { + transform: translateX(0) scaleX(25%); + } + 100% { + transform: translateX(100%) scaleX(25%); + } +} diff --git a/libs/design/progress-bar/src/progress-bar.component.spec.ts b/libs/design/progress-bar/src/progress-bar.component.spec.ts new file mode 100644 index 0000000000..65e1faaba9 --- /dev/null +++ b/libs/design/progress-bar/src/progress-bar.component.spec.ts @@ -0,0 +1,109 @@ +import { + Component, + DebugElement, +} from '@angular/core'; +import { + waitForAsync, + ComponentFixture, + TestBed, + flush, + fakeAsync, +} from '@angular/core/testing'; +import { By } from '@angular/platform-browser'; +import { NoopAnimationsModule } from '@angular/platform-browser/animations'; + +import { DaffPalette } from '@daffodil/design'; + +import { DaffProgressBarComponent } from './progress-bar.component'; + +@Component({ + template: ` + + + `, +}) +class WrapperComponent { + color: DaffPalette; + percentage: number; + onAnimationComplete(): void {}; +} + +describe('DaffProgressBarComponent', () => { + let fixture: ComponentFixture; + let de: DebugElement; + let wrapper: WrapperComponent; + let component: DaffProgressBarComponent; + + beforeEach(waitForAsync(() => { + TestBed.configureTestingModule({ + imports: [ + NoopAnimationsModule, + ], + declarations: [ + WrapperComponent, + DaffProgressBarComponent, + ], + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(WrapperComponent); + wrapper = fixture.componentInstance; + de = fixture.debugElement.query(By.css('daff-progress-bar')); + component = de.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + describe('', () => { + it('should add a class of "daff-progress-bar" to the host element', () => { + expect(de.classes).toEqual(jasmine.objectContaining({ + 'daff-progress-bar': true, + })); + }); + }); + + it('should be able to take `percentage` as an input', () =>{ + wrapper.percentage = 20; + fixture.detectChanges(); + + expect(component.percentage).toEqual(20); + }); + + /** + * This needs to be in a fakeAsync as there are animationFrames being processed + */ + it('should emit `finished` when the progress bar is filled and the animation is complete', fakeAsync(() => { + wrapper.percentage = 100; + spyOn(wrapper, 'onAnimationComplete'); + + fixture.detectChanges(); + flush(); + + expect(wrapper.onAnimationComplete).toHaveBeenCalledTimes(1); + })); + + it('should be unfilled by default', () => { + wrapper.percentage = 0; + fixture.detectChanges(); + + expect(component.percentage).toEqual(0); + }); + + describe('using a colored variant of a progress bar', () => { + it('should set a color class on the progress bar', () => { + wrapper.color = 'primary'; + fixture.detectChanges(); + + expect(de.nativeElement.classList.contains('daff-primary')).toEqual(true); + }); + + it('should set the default color to primary', () => { + expect(de.nativeElement.classList.contains('daff-primary')).toEqual(true); + }); + }); +}); diff --git a/libs/design/progress-bar/src/progress-bar.component.ts b/libs/design/progress-bar/src/progress-bar.component.ts new file mode 100644 index 0000000000..bc85fd3ce8 --- /dev/null +++ b/libs/design/progress-bar/src/progress-bar.component.ts @@ -0,0 +1,143 @@ +import { AnimationEvent } from '@angular/animations'; +import { coerceBooleanProperty } from '@angular/cdk/coercion'; +import { + Component, + Input, + ChangeDetectionStrategy, + ElementRef, + Output, + EventEmitter, + Renderer2, + HostBinding, + ChangeDetectorRef, +} from '@angular/core'; + +import { + daffColorMixin, + DaffColorable, +} from '@daffodil/design'; + +import { daffProgressBarAnimation } from './animation/progress-bar-animation'; + +/** + * An _elementRef and an instance of renderer2 are needed for the Colorable mixin + */ +class DaffProgressBarBase{ + constructor(public _elementRef: ElementRef, public _renderer: Renderer2) {} +} + +const _daffProgressBarBase = daffColorMixin(DaffProgressBarBase, 'primary'); + +export const clamp = (number: number, min: number, max: number) => Math.min(Math.max(number, min), max); + +/** + * @inheritdoc + */ +@Component({ + selector: 'daff-progress-bar', + templateUrl: './progress-bar.component.html', + styleUrls: ['./progress-bar.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, + //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'], + animations: [ + daffProgressBarAnimation.fill, + ], +}) +export class DaffProgressBarComponent extends _daffProgressBarBase implements DaffColorable { + + /** + * @docs-private + */ + @HostBinding('class.daff-progress-bar') class = true; + + /** + * @docs-private + */ + @HostBinding('class.indeterminate') get indeterminateClass() { + return this._indeterminate; + } + + @HostBinding('role') get role() { + return 'progressbar'; + } + + @HostBinding('attr.aria-label') get ariaLabel() { + return this._indeterminate ? 'loading' : null; + } + + @HostBinding('attr.aria-valuemin') ariaValueMin = '0'; + @HostBinding('attr.aria-valuemax') ariaValueMax = '100'; + @HostBinding('attr.aria-valuenow') get ariaValueNow() { + return this.percentage; + } + + constructor( + private _changeDetectorRef: ChangeDetectorRef, + private elementRef: ElementRef, + private renderer: Renderer2, + ) { + super(elementRef, renderer); + } + + private _percentage = 0; + private _indeterminate = false; + + /** + * Sets the percentage completion of the progression, + * expressed as a whole number between 0 and 100. + * + */ + @Input() get percentage(): number { + return this._percentage; + }; + set percentage(val: number) { + this._percentage = clamp(val, 0, 100); + this._changeDetectorRef.markForCheck(); + } + + /** + * Property to set the animation of a progress bar to + * run for an indefinite amount of time. + * + * See: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/progress + **/ + @Input() get indeterminate() { + return this._indeterminate; + } + set indeterminate(value: any) { + this._indeterminate = coerceBooleanProperty(value); + } + + /** + * An event that emits each time the progression reaches 100% + * and the animation is finished + */ + @Output() finished: EventEmitter = new EventEmitter(); + + /** + * Calculates when the progress animation is fully completed + * + * @param event: AnimationEvent + */ + onAnimationComplete(event: AnimationEvent): void { + // @ts-expect-error: @angular/animations typing error on event.toState as string + // See: https://github.com/angular/angular/issues/26507 + if(event.toState === '100' || event.toState === 100) { + this.finished.emit(); + } + } + + /** + * @docs-private + */ + get fillState(): any { + return { + value: this.percentage, + params: { + percentage: this.percentage, + }, + }; + } +} diff --git a/libs/design/progress-bar/src/progress-bar.module.ts b/libs/design/progress-bar/src/progress-bar.module.ts new file mode 100644 index 0000000000..6cd008f0e2 --- /dev/null +++ b/libs/design/progress-bar/src/progress-bar.module.ts @@ -0,0 +1,17 @@ +import { CommonModule } from '@angular/common'; +import { NgModule } from '@angular/core'; + +import { DaffProgressBarComponent } from './progress-bar.component'; + +@NgModule({ + imports: [ + CommonModule, + ], + declarations: [ + DaffProgressBarComponent, + ], + exports: [ + DaffProgressBarComponent, + ], +}) +export class DaffProgressBarModule { } diff --git a/libs/design/progress-bar/src/public_api.ts b/libs/design/progress-bar/src/public_api.ts new file mode 100644 index 0000000000..c4cf498eed --- /dev/null +++ b/libs/design/progress-bar/src/public_api.ts @@ -0,0 +1,2 @@ +export { DaffProgressBarModule } from './progress-bar.module'; +export * from './progress-bar.component'; diff --git a/libs/design/scss/theme.scss b/libs/design/scss/theme.scss index b2a6c08f7d..a5a362d2cf 100644 --- a/libs/design/scss/theme.scss +++ b/libs/design/scss/theme.scss @@ -38,6 +38,7 @@ @use '../notification/src/notification-theme' as notification; @use '../paginator/src/paginator-theme' as paginator; @use '../sidebar/src/sidebar-theme' as sidebar; +@use '../progress-bar/src/progress-bar-theme' as progress-bar; @use '../scss/state/skeleton/mixins' as skeleton; @use '../tree/src/tree-theme' as tree; @use '../toast/src/toast-theme' as toast; @@ -66,6 +67,7 @@ @include native-select.daff-native-select-theme($theme); @include loading-icon.daff-loading-icon-theme($theme); @include progress-indicator.daff-progress-indicator-theme($theme); + @include progress-bar.daff-progress-bar-theme($theme); // Molecules @include accordion.daff-accordion-theme($theme); diff --git a/libs/design/src/atoms/progress-indicator/progress-indicator.component.ts b/libs/design/src/atoms/progress-indicator/progress-indicator.component.ts index 6a70f8c33f..e9da1968b2 100644 --- a/libs/design/src/atoms/progress-indicator/progress-indicator.component.ts +++ b/libs/design/src/atoms/progress-indicator/progress-indicator.component.ts @@ -27,7 +27,8 @@ class DaffProgressIndicatorBase{ const _daffProgressIndicatorBase = daffColorMixin(DaffProgressIndicatorBase, 'primary'); /** - * @inheritdoc + * @deprecated in v1.0.0 + * Use `DaffProgressBarComponent` instead. */ @Component({ selector: 'daff-progress-indicator',