diff --git a/.stylelintrc b/.stylelintrc index 539ae25aa6..434129b1cf 100644 --- a/.stylelintrc +++ b/.stylelintrc @@ -82,7 +82,9 @@ "selector-pseudo-class-parentheses-space-inside": "never", "selector-pseudo-element-case": "lower", "selector-pseudo-element-colon-notation": "double", - "selector-pseudo-element-no-unknown": true, + "selector-pseudo-element-no-unknown": [true, { + "ignorePseudoElements": ["ng-deep"] + }], "selector-type-case": "lower", "selector-max-id": 0, "no-missing-end-of-source-newline": true, diff --git a/apps/design-land/src/app/app-routing.module.ts b/apps/design-land/src/app/app-routing.module.ts index 5c22a35c55..6dbac17975 100644 --- a/apps/design-land/src/app/app-routing.module.ts +++ b/apps/design-land/src/app/app-routing.module.ts @@ -26,6 +26,7 @@ export const appRoutes: Routes = [ { path: 'image', loadChildren: () => import('./image/image.module').then(m => m.DesignLandImageModule) }, { path: 'image-gallery', loadChildren: () => import('./image-gallery/image-gallery.module').then(m => m.ImageGalleryModule) }, { path: 'navbar', loadChildren: () => import('./navbar/navbar.module').then(m => m.NavbarModule) }, + { path: 'media-gallery', loadChildren: () => import('./media-gallery/media-gallery.module').then(m => m.DesignLandMediaGalleryModule) }, { path: 'modal', loadChildren: () => import('./modal/modal.module').then(m => m.ModalModule) }, { path: 'paginator', loadChildren: () => import('./paginator/paginator.module').then(m => m.PaginatorModule) }, { path: 'progress-indicator', loadChildren: () => import('./progress-indicator/progress-indicator.module').then(m => m.ProgressIndicatorModule) }, diff --git a/apps/design-land/src/app/app.component.html b/apps/design-land/src/app/app.component.html index beaa075215..e48ea4d22e 100644 --- a/apps/design-land/src/app/app.component.html +++ b/apps/design-land/src/app/app.component.html @@ -28,6 +28,7 @@ Link Set List Navigation Bar + Media Gallery Modal Paginator Quantity Dropdown diff --git a/apps/design-land/src/app/media-gallery/media-gallery-routing-module.ts b/apps/design-land/src/app/media-gallery/media-gallery-routing-module.ts new file mode 100644 index 0000000000..268bf4c871 --- /dev/null +++ b/apps/design-land/src/app/media-gallery/media-gallery-routing-module.ts @@ -0,0 +1,21 @@ +import { NgModule } from '@angular/core'; +import { + Routes, + RouterModule, +} from '@angular/router'; + +import { DesignLandMediaGalleryComponent } from './media-gallery.component'; + +export const mediaGalleryRoutes: Routes = [ + { path: '', component: DesignLandMediaGalleryComponent }, +]; + +@NgModule({ + imports: [ + RouterModule.forChild(mediaGalleryRoutes), + ], + exports: [ + RouterModule, + ], +}) +export class DesignLandMediaGalleryRoutingModule {} diff --git a/apps/design-land/src/app/media-gallery/media-gallery.component.html b/apps/design-land/src/app/media-gallery/media-gallery.component.html new file mode 100644 index 0000000000..4057934c86 --- /dev/null +++ b/apps/design-land/src/app/media-gallery/media-gallery.component.html @@ -0,0 +1,19 @@ + +

Media Gallery

+

<daff-media-gallery> is used to display a group of [daffThumbnail]s in a gallery format. Media galleries are useful to showcase multiple images related to a single product or topic.

+ +

Thumbnail

+

[daffThumbnail]should be used as a directive with <daff-image>. (View Image Documentation)

+

It should never be used as a standalone component. The first thumbnail is selected by default and dynamically rendered as the primary image by utilizing the <daff-media-renderer> component. The selected thumbnail can be controlled by the enduser, and the position of the list of thumbnails is dependent on the screen size.

+ + + +

Image Aspect Ratio

+

It's recommended to utilize the same aspect ratio for all images in the same media gallery. Otherwise, the height and width of the media gallery may change with every different aspect ratio presented by the selected thumbnail as show in the example.

+ +

The thumbnail dimension is set to 80x80 pixels, so the recommended aspect ratio is 1:1. However, it is not required since the thumbnail will horizontally and vertically center align images within a thumbnail.

+ + +

Accessibility

+

Accessibility considerations for media gallery is handled by the DaffImageComponent. The alt attribute must be defined in <daff-image>. It specifies an alternate text for an image. An error will appear if it's not defined. This is important because it allows screen readers to describe what's in the image for visually impaired people. (View Image Documentation)

+
\ No newline at end of file diff --git a/apps/design-land/src/app/media-gallery/media-gallery.component.scss b/apps/design-land/src/app/media-gallery/media-gallery.component.scss new file mode 100644 index 0000000000..e69de29bb2 diff --git a/apps/design-land/src/app/media-gallery/media-gallery.component.spec.ts b/apps/design-land/src/app/media-gallery/media-gallery.component.spec.ts new file mode 100644 index 0000000000..431cb51128 --- /dev/null +++ b/apps/design-land/src/app/media-gallery/media-gallery.component.spec.ts @@ -0,0 +1,29 @@ +import { + async, + ComponentFixture, + TestBed, +} from '@angular/core/testing'; + +import { DesignLandMediaGalleryComponent } from './media-gallery.component'; + +describe('DesignLandMediaGalleryComponent', () => { + let component: DesignLandMediaGalleryComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ DesignLandMediaGalleryComponent ], + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(DesignLandMediaGalleryComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/apps/design-land/src/app/media-gallery/media-gallery.component.ts b/apps/design-land/src/app/media-gallery/media-gallery.component.ts new file mode 100644 index 0000000000..c24aba1537 --- /dev/null +++ b/apps/design-land/src/app/media-gallery/media-gallery.component.ts @@ -0,0 +1,11 @@ +import { + Component, + OnInit, +} from '@angular/core'; + +@Component({ + selector: 'design-land-media-gallery', + templateUrl: './media-gallery.component.html', + styleUrls: ['./media-gallery.component.scss'], +}) +export class DesignLandMediaGalleryComponent {} diff --git a/apps/design-land/src/app/media-gallery/media-gallery.module.ts b/apps/design-land/src/app/media-gallery/media-gallery.module.ts new file mode 100644 index 0000000000..26171ff0bf --- /dev/null +++ b/apps/design-land/src/app/media-gallery/media-gallery.module.ts @@ -0,0 +1,53 @@ +import { CommonModule } from '@angular/common'; +import { + NgModule, + Injector, + ComponentFactoryResolver, +} from '@angular/core'; +import { createCustomElement } from '@angular/elements'; + + +import { + DaffArticleModule, + DaffMediaGalleryModule, + DaffImageModule, +} from '@daffodil/design'; +import { MEDIA_GALLERY_EXAMPLES } from '@daffodil/design/media-gallery/examples'; + +import { DesignLandExampleViewerModule } from '../core/code-preview/container/example-viewer.module'; +import { DesignLandMediaGalleryRoutingModule } from './media-gallery-routing-module'; +import { DesignLandMediaGalleryComponent } from './media-gallery.component'; + + +@NgModule({ + declarations: [ + DesignLandMediaGalleryComponent, + ], + imports: [ + CommonModule, + DesignLandMediaGalleryRoutingModule, + DesignLandExampleViewerModule, + + DaffArticleModule, + DaffMediaGalleryModule, + DaffImageModule, + ], +}) +export class DesignLandMediaGalleryModule { + constructor( + injector: Injector, + private componentFactoryResolver: ComponentFactoryResolver, + ) { + MEDIA_GALLERY_EXAMPLES.map((classConstructor) => ({ + element: createCustomElement(classConstructor, { injector }), + class: classConstructor, + })) + .map((customElement) => { + // Register the custom element with the browser. + customElements.define( + this.componentFactoryResolver.resolveComponentFactory(customElement.class).selector + '-example', + customElement.element, + ); + }); + } +} diff --git a/libs/design/media-gallery/examples/ng-package.json b/libs/design/media-gallery/examples/ng-package.json new file mode 100644 index 0000000000..6c2fd1915f --- /dev/null +++ b/libs/design/media-gallery/examples/ng-package.json @@ -0,0 +1,8 @@ +{ + "$schema": "../../../../node_modules/ng-packagr/ng-package.schema.json", + "dest": "../../../dist/design/examples", + "deleteDestPath": false, + "lib": { + "entryFile": "src/index.ts" + } +} \ No newline at end of file diff --git a/libs/design/media-gallery/examples/package.json b/libs/design/media-gallery/examples/package.json new file mode 100644 index 0000000000..773d0874c4 --- /dev/null +++ b/libs/design/media-gallery/examples/package.json @@ -0,0 +1,3 @@ +{ + "name": "@daffodil/design/media-gallery/examples" +} \ No newline at end of file diff --git a/libs/design/media-gallery/examples/src/basic-media-gallery/basic-media-gallery.component.html b/libs/design/media-gallery/examples/src/basic-media-gallery/basic-media-gallery.component.html new file mode 100644 index 0000000000..ee23f5ca4a --- /dev/null +++ b/libs/design/media-gallery/examples/src/basic-media-gallery/basic-media-gallery.component.html @@ -0,0 +1,7 @@ + + + + + + + diff --git a/libs/design/media-gallery/examples/src/basic-media-gallery/basic-media-gallery.component.ts b/libs/design/media-gallery/examples/src/basic-media-gallery/basic-media-gallery.component.ts new file mode 100644 index 0000000000..d1250a1e7f --- /dev/null +++ b/libs/design/media-gallery/examples/src/basic-media-gallery/basic-media-gallery.component.ts @@ -0,0 +1,11 @@ +import { Component } from '@angular/core'; + +@Component({ + // eslint-disable-next-line @angular-eslint/component-selector + selector: 'basic-media-gallery', + templateUrl: './basic-media-gallery.component.html', +}) +export class BasicMediaGalleryComponent { + + +} diff --git a/libs/design/media-gallery/examples/src/basic-media-gallery/basic-media-gallery.module.ts b/libs/design/media-gallery/examples/src/basic-media-gallery/basic-media-gallery.module.ts new file mode 100644 index 0000000000..2237f17313 --- /dev/null +++ b/libs/design/media-gallery/examples/src/basic-media-gallery/basic-media-gallery.module.ts @@ -0,0 +1,23 @@ +import { NgModule } from '@angular/core'; + +import { + DaffMediaGalleryModule, + DaffImageModule, +} from '@daffodil/design'; + +import { BasicMediaGalleryComponent } from './basic-media-gallery.component'; + +@NgModule({ + declarations: [ + BasicMediaGalleryComponent, + ], + exports: [ + BasicMediaGalleryComponent, + ], + imports: [ + DaffImageModule, + DaffMediaGalleryModule, + ], + providers: [], +}) +export class BasicMediaGalleryModule { } diff --git a/libs/design/media-gallery/examples/src/examples.ts b/libs/design/media-gallery/examples/src/examples.ts new file mode 100644 index 0000000000..356fdd8429 --- /dev/null +++ b/libs/design/media-gallery/examples/src/examples.ts @@ -0,0 +1,7 @@ +import { BasicMediaGalleryComponent } from './basic-media-gallery/basic-media-gallery.component'; +import { MismatchedSizesMediaGalleryComponent } from './mismatched-sizes-media-gallery/mismatched-sizes-media-gallery.component'; + +export const MEDIA_GALLERY_EXAMPLES = [ + BasicMediaGalleryComponent, + MismatchedSizesMediaGalleryComponent, +]; diff --git a/libs/design/media-gallery/examples/src/index.ts b/libs/design/media-gallery/examples/src/index.ts new file mode 100644 index 0000000000..4aaf8f92ed --- /dev/null +++ b/libs/design/media-gallery/examples/src/index.ts @@ -0,0 +1 @@ +export * from './public_api'; diff --git a/libs/design/media-gallery/examples/src/mismatched-sizes-media-gallery/mismatched-sizes-media-gallery.component.html b/libs/design/media-gallery/examples/src/mismatched-sizes-media-gallery/mismatched-sizes-media-gallery.component.html new file mode 100644 index 0000000000..227b8587de --- /dev/null +++ b/libs/design/media-gallery/examples/src/mismatched-sizes-media-gallery/mismatched-sizes-media-gallery.component.html @@ -0,0 +1,7 @@ + + + + + + + diff --git a/libs/design/media-gallery/examples/src/mismatched-sizes-media-gallery/mismatched-sizes-media-gallery.component.ts b/libs/design/media-gallery/examples/src/mismatched-sizes-media-gallery/mismatched-sizes-media-gallery.component.ts new file mode 100644 index 0000000000..93e2375e99 --- /dev/null +++ b/libs/design/media-gallery/examples/src/mismatched-sizes-media-gallery/mismatched-sizes-media-gallery.component.ts @@ -0,0 +1,8 @@ +import { Component } from '@angular/core'; + +@Component({ + // eslint-disable-next-line @angular-eslint/component-selector + selector: 'mismatched-sizes-media-gallery', + templateUrl: './mismatched-sizes-media-gallery.component.html', +}) +export class MismatchedSizesMediaGalleryComponent {} diff --git a/libs/design/media-gallery/examples/src/mismatched-sizes-media-gallery/mismatched-sizes-media-gallery.module.ts b/libs/design/media-gallery/examples/src/mismatched-sizes-media-gallery/mismatched-sizes-media-gallery.module.ts new file mode 100644 index 0000000000..abe5c6eb79 --- /dev/null +++ b/libs/design/media-gallery/examples/src/mismatched-sizes-media-gallery/mismatched-sizes-media-gallery.module.ts @@ -0,0 +1,23 @@ +import { NgModule } from '@angular/core'; + +import { + DaffMediaGalleryModule, + DaffImageModule, +} from '@daffodil/design'; + +import { MismatchedSizesMediaGalleryComponent } from './mismatched-sizes-media-gallery.component'; + +@NgModule({ + declarations: [ + MismatchedSizesMediaGalleryComponent, + ], + exports: [ + MismatchedSizesMediaGalleryComponent, + ], + imports: [ + DaffImageModule, + DaffMediaGalleryModule, + ], + providers: [], +}) +export class MismatchedSizesMediaGalleryModule { } diff --git a/libs/design/media-gallery/examples/src/public_api.ts b/libs/design/media-gallery/examples/src/public_api.ts new file mode 100644 index 0000000000..e29171589a --- /dev/null +++ b/libs/design/media-gallery/examples/src/public_api.ts @@ -0,0 +1,4 @@ +export { MEDIA_GALLERY_EXAMPLES } from './examples'; + +export { BasicMediaGalleryModule } from './basic-media-gallery/basic-media-gallery.module'; +export { MismatchedSizesMediaGalleryModule } from './mismatched-sizes-media-gallery/mismatched-sizes-media-gallery.module'; diff --git a/libs/design/src/atoms/image/image.component.html b/libs/design/src/atoms/image/image.component.html index e93f757252..3d4f87c23b 100644 --- a/libs/design/src/atoms/image/image.component.html +++ b/libs/design/src/atoms/image/image.component.html @@ -1,3 +1,3 @@ -
+
\ No newline at end of file diff --git a/libs/design/src/atoms/image/image.component.scss b/libs/design/src/atoms/image/image.component.scss index 041543803a..68a97aad95 100644 --- a/libs/design/src/atoms/image/image.component.scss +++ b/libs/design/src/atoms/image/image.component.scss @@ -2,20 +2,23 @@ :host { display: inline-block; + position: relative; width: 100%; img { position: absolute; left: 0; + right: 0; top: 0; + bottom: 0; height: auto; + margin: auto; max-width: 100%; } } .daff-image { &__wrapper { - position: relative; height: 0; } } diff --git a/libs/design/src/atoms/image/image.component.ts b/libs/design/src/atoms/image/image.component.ts index cbf677a912..d78705fb3a 100644 --- a/libs/design/src/atoms/image/image.component.ts +++ b/libs/design/src/atoms/image/image.component.ts @@ -9,6 +9,8 @@ import { } from '@angular/core'; import { DomSanitizer } from '@angular/platform-browser'; +import { daffThumbnailCompatToken } from '../../molecules/media-gallery/public_api'; + const validateProperty = (object: Record, prop: string) => { if (object[prop] === null || object[prop] === undefined || object[prop] === '') { throw new Error(`DaffImageComponent must have a defined ${prop} attribute.`); @@ -35,6 +37,12 @@ const validateProperties = (object: Record, props: string[]) => { templateUrl: './image.component.html', styleUrls: ['./image.component.scss'], changeDetection: ChangeDetectionStrategy.OnPush, + providers: [ + { + // eslint-disable-next-line @typescript-eslint/no-use-before-define + provide: daffThumbnailCompatToken, useExisting: DaffImageComponent, + }, + ], }) export class DaffImageComponent implements OnInit { @@ -98,7 +106,7 @@ export class DaffImageComponent implements OnInit { /** * @docs-private */ - get paddingTop(): any { + get _paddingTop(): any { if (!this.height || !this.width ) { return undefined; } diff --git a/libs/design/src/atoms/image/image.module.ts b/libs/design/src/atoms/image/image.module.ts index d934468ccf..6c6e48cbb6 100644 --- a/libs/design/src/atoms/image/image.module.ts +++ b/libs/design/src/atoms/image/image.module.ts @@ -13,5 +13,8 @@ import { DaffImageComponent } from './image.component'; exports: [ DaffImageComponent, ], + entryComponents: [ + DaffImageComponent, + ], }) export class DaffImageModule { } diff --git a/libs/design/src/molecules/media-gallery/README.md b/libs/design/src/molecules/media-gallery/README.md new file mode 100644 index 0000000000..b731d81bca --- /dev/null +++ b/libs/design/src/molecules/media-gallery/README.md @@ -0,0 +1,19 @@ +# Media Gallery +`` is used to display a group of `[daffThumbnail]`s in a gallery format. Media galleries are useful to showcase multiple images related to a single product or topic. + +## Thumbnail +`[daffThumbnail]` should be used as a directive with ``. [View Image Documentation](/libs/design/src/atoms/image/README.md) + +It should never be used as a standalone component. The first thumbnail is selected by default and dynamically rendered as the primary image by utilizing the `` component. The selected thumbnail can be controlled by the enduser, and the position of the list of thumbnails is dependent on the screen size. + + + +## Image Aspect Ratio +It's recommended to utilize the same aspect ratio for all images in the same media gallery. Otherwise, the height and width of the media gallery may change with every different aspect ratio presented by the selected thumbnail as show in the example. + +The thumbnail dimension is set to `80x80` pixels, so the recommended aspect ratio is `1:1`. However, it is not required since the thumbnail will horizontally and vertically center align images within a thumbnail. + + + +## Accessibility +Accessibility considerations for media gallery is handled by the `DaffImageComponent`. The `alt` attribute must be defined in ``. It specifies an alternate text for an image. An error will appear if it's not defined. This is important because it allows screen readers to describe what's in the image for visually impaired people. [View Image Documentation](/libs/design/src/atoms/image/README.md) \ No newline at end of file diff --git a/libs/design/src/molecules/media-gallery/media-gallery-theme.scss b/libs/design/src/molecules/media-gallery/media-gallery-theme.scss new file mode 100644 index 0000000000..0cfcce1419 --- /dev/null +++ b/libs/design/src/molecules/media-gallery/media-gallery-theme.scss @@ -0,0 +1,22 @@ +@mixin daff-media-gallery-theme($theme) { + $primary: map-get($theme, primary); + $secondary: map-get($theme, secondary); + $tertiary: map-get($theme, tertiary); + $base: daff-map-deep-get($theme, 'core.base'); + $white: daff-map-deep-get($theme, 'core.white'); + $black: daff-map-deep-get($theme, 'core.black'); + $gray: daff-configure-palette($daff-gray, 60); + + .daff-media-gallery { + $root: &; + + .daff-thumbnail { + border: 1px solid transparent; + transition: border 150ms; + + &--selected { + border: 1px solid daff-color($gray); + } + } + } +} diff --git a/libs/design/src/molecules/media-gallery/media-gallery.component.html b/libs/design/src/molecules/media-gallery/media-gallery.component.html new file mode 100644 index 0000000000..8bbfc6f96b --- /dev/null +++ b/libs/design/src/molecules/media-gallery/media-gallery.component.html @@ -0,0 +1,6 @@ + + \ No newline at end of file diff --git a/libs/design/src/molecules/media-gallery/media-gallery.component.scss b/libs/design/src/molecules/media-gallery/media-gallery.component.scss new file mode 100644 index 0000000000..49783ba5d2 --- /dev/null +++ b/libs/design/src/molecules/media-gallery/media-gallery.component.scss @@ -0,0 +1,60 @@ +@import '../../scss/daff-util'; + +:host(.daff-media-gallery) { + /* autoprefixer grid: autoplace */ + $root: '.daff-media-gallery'; + display: flex; + flex-direction: column; + + @include breakpoint(big-tablet) { + flex-direction: row; + } + + ::ng-deep { + .daff-thumbnail { + @include clickable(); + display: inline-block; + height: calc(25% - 8px); + width: calc(25% - 8px); + margin: 0 4px; + max-width: 100%; + overflow: hidden; + user-select: none; + + @include breakpoint(mobile) { + height: calc(15% - 8px); + width: calc(15% - 8px); + } + + @include breakpoint(big-tablet) { + display: block; + margin: 0 0 8px; + height: 80px; + width: 80px; + } + } + } +} + +.daff-media-gallery { + &__thumbnails { + margin: 0 -4px; + max-height: 100%; + order: 2; + + @include breakpoint(big-tablet) { + margin: 0 8px 0 0; + order: 1; + } + } + + &__selected-thumbnail { + display: block; + flex-grow: 1; + order: 1; + + @include breakpoint(big-tablet) { + order: 2; + } + } +} diff --git a/libs/design/src/molecules/media-gallery/media-gallery.component.spec.ts b/libs/design/src/molecules/media-gallery/media-gallery.component.spec.ts new file mode 100644 index 0000000000..6cead74056 --- /dev/null +++ b/libs/design/src/molecules/media-gallery/media-gallery.component.spec.ts @@ -0,0 +1,113 @@ +import { + Component, + DebugElement, +} from '@angular/core'; +import { + ComponentFixture, + TestBed, + waitForAsync, +} from '@angular/core/testing'; +import { By } from '@angular/platform-browser'; + +import { DaffMediaGalleryComponent } from './media-gallery.component'; +import { DaffMediaGalleryRegistry } from './registry/media-gallery.registry'; +import { daffThumbnailCompatToken } from './thumbnail/thumbnail-compat.token'; +import { DaffThumbnailDirective } from './thumbnail/thumbnail.directive'; + +@Component({ + template: ` +
+
`, +}) +class WrapperComponent { + nameValue: number; +} + +@Component({ + template: '', + selector: 'daff-media-renderer', +}) +class MockMediaRendererComponent {} + +describe('DaffMediaGalleryComponent', () => { + let wrapper: WrapperComponent; + let fixture: ComponentFixture; + let de: DebugElement; + let component: DaffMediaGalleryComponent; + const stubName = 3; + + beforeEach(waitForAsync(() => { + TestBed.configureTestingModule({ + declarations: [ + WrapperComponent, + MockMediaRendererComponent, + DaffMediaGalleryComponent, + DaffThumbnailDirective, + ], + providers: [ + { provide: daffThumbnailCompatToken, useValue: DaffThumbnailDirective }, + DaffMediaGalleryRegistry, + ], + }).compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(WrapperComponent); + wrapper = fixture.componentInstance; + wrapper.nameValue = stubName; + fixture.detectChanges(); + + de = fixture.debugElement.query(By.css('daff-media-gallery')); + component = de.componentInstance; + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + it('should add a daff-media-gallery class to the host element', () => { + expect(de.classes).toEqual(jasmine.objectContaining({ + 'daff-media-gallery': true, + })); + }); + + it('should take a name as input', () => { + expect(component.name).toEqual(stubName); + }); +}); + +@Component({ + template: '', +}) +class DefaultWrapperComponent {} + +describe('DaffMediaGalleryComponent - default', () => { + let wrapper: DefaultWrapperComponent; + let fixture: ComponentFixture; + let de: DebugElement; + let component: DaffMediaGalleryComponent; + + beforeEach(waitForAsync(() => { + TestBed.configureTestingModule({ + declarations: [ + DaffMediaGalleryComponent, + MockMediaRendererComponent, + DaffThumbnailDirective, + DefaultWrapperComponent, + ], + }).compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(DefaultWrapperComponent); + wrapper = fixture.componentInstance; + fixture.detectChanges(); + + de = fixture.debugElement.query(By.css('daff-media-gallery')); + component = de.componentInstance; + }); + + it('should set the name to a unique id if a name is not provided', () => { + expect(component.name).toEqual(jasmine.any(Number)); + }); +}); diff --git a/libs/design/src/molecules/media-gallery/media-gallery.component.ts b/libs/design/src/molecules/media-gallery/media-gallery.component.ts new file mode 100644 index 0000000000..4471a67f10 --- /dev/null +++ b/libs/design/src/molecules/media-gallery/media-gallery.component.ts @@ -0,0 +1,30 @@ +import { + Component, + HostBinding, + ChangeDetectionStrategy, + Input, +} from '@angular/core'; + +let uniqueGalleryId = 0; + +@Component({ + selector: 'daff-media-gallery', + templateUrl: './media-gallery.component.html', + styleUrls: ['./media-gallery.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class DaffMediaGalleryComponent { + /** + * Adds a class for styling the media gallery + */ + @HostBinding('class.daff-media-gallery') class = true; + + /** + * The name of the gallery + */ + @Input() name = uniqueGalleryId; + + constructor() { + uniqueGalleryId++; + } +} diff --git a/libs/design/src/molecules/media-gallery/media-gallery.module.ts b/libs/design/src/molecules/media-gallery/media-gallery.module.ts new file mode 100644 index 0000000000..954a08a87e --- /dev/null +++ b/libs/design/src/molecules/media-gallery/media-gallery.module.ts @@ -0,0 +1,22 @@ +import { CommonModule } from '@angular/common'; +import { NgModule } from '@angular/core'; + +import { DaffMediaGalleryComponent } from './media-gallery.component'; +import { DaffMediaRendererComponent } from './media-renderer/media-renderer.component'; +import { DaffThumbnailDirective } from './thumbnail/thumbnail.directive'; + +@NgModule({ + declarations: [ + DaffMediaGalleryComponent, + DaffThumbnailDirective, + DaffMediaRendererComponent, + ], + imports: [ + CommonModule, + ], + exports: [ + DaffThumbnailDirective, + DaffMediaGalleryComponent, + ], +}) +export class DaffMediaGalleryModule {} diff --git a/libs/design/src/molecules/media-gallery/media-renderer/media-renderer.component.html b/libs/design/src/molecules/media-gallery/media-renderer/media-renderer.component.html new file mode 100644 index 0000000000..e2a7a80ddd --- /dev/null +++ b/libs/design/src/molecules/media-gallery/media-renderer/media-renderer.component.html @@ -0,0 +1 @@ + diff --git a/libs/design/src/molecules/media-gallery/media-renderer/media-renderer.component.spec.ts b/libs/design/src/molecules/media-gallery/media-renderer/media-renderer.component.spec.ts new file mode 100644 index 0000000000..afc26cda15 --- /dev/null +++ b/libs/design/src/molecules/media-gallery/media-renderer/media-renderer.component.spec.ts @@ -0,0 +1,106 @@ +import { + Component, + Type, +} from '@angular/core'; +import { + ComponentFixture, + TestBed, + waitForAsync, +} from '@angular/core/testing'; +import { By } from '@angular/platform-browser'; +import { BehaviorSubject } from 'rxjs'; + +import { DaffArticleComponent } from '../../article/public_api'; +import { DaffCardComponent } from '../../card/public_api'; +import { DaffMediaGalleryComponent } from '../media-gallery.component'; +import { DaffMediaGalleryRegistry } from '../registry/media-gallery.registry'; +import { daffThumbnailCompatToken } from '../thumbnail/thumbnail-compat.token'; +import { DaffThumbnailDirective } from '../thumbnail/thumbnail.directive'; +import { DaffMediaRendererComponent } from './media-renderer.component'; + +@Component({ + selector: 'daff-mock-thumbnail1', + template: '', + providers: [ + { + // eslint-disable-next-line @typescript-eslint/no-use-before-define + provide: daffThumbnailCompatToken, useExisting: DaffMockThumbnail1Component, + }, + ], +}) +export class DaffMockThumbnail1Component {} + +@Component({ + selector: 'daff-mock-thumbnail2', + template: '', + providers: [ + { + // eslint-disable-next-line @typescript-eslint/no-use-before-define + provide: daffThumbnailCompatToken, useExisting: DaffMockThumbnail2Component, + }, + ], +}) +export class DaffMockThumbnail2Component {} + +describe('DaffMediaRendererComponent', () => { + let component: DaffMediaRendererComponent; + let fixture: ComponentFixture; + let registry: DaffMediaGalleryRegistry; + let mockGallery: DaffMediaGalleryComponent; + let mockThumbnail1: DaffThumbnailDirective; + let mockThumbnail2: DaffThumbnailDirective; + + beforeEach(waitForAsync(() => { + mockGallery = new DaffMediaGalleryComponent(); + TestBed.configureTestingModule({ + declarations: [ + DaffMediaRendererComponent, + DaffArticleComponent, + DaffCardComponent, + DaffMockThumbnail1Component, + DaffMockThumbnail2Component, + ], + providers: [ + { + provide: DaffMediaGalleryRegistry, + useValue: jasmine.createSpyObj('DaffMediaGalleryRegistry', ['add', 'remove', 'select']), + }, + { + provide: DaffMediaGalleryComponent, + useValue: mockGallery, + }, + ], + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(DaffMediaRendererComponent); + registry = TestBed.inject(DaffMediaGalleryRegistry); + mockThumbnail1 = new DaffThumbnailDirective(null, jasmine.createSpyObj('ChangeDetectorRef', ['markForCheck']), null, mockGallery); + mockThumbnail1.component = >(new DaffArticleComponent()); + mockThumbnail2 = new DaffThumbnailDirective(null, jasmine.createSpyObj('ChangeDetectorRef', ['markForCheck']), null, mockGallery); + mockThumbnail2.component = >(new DaffCardComponent(null, null)); + mockThumbnail1.selected = true; + mockThumbnail2.selected = false; + registry.galleries = { + [mockGallery.name]: new BehaviorSubject({ + gallery: mockGallery, + thumbnails: [mockThumbnail1, mockThumbnail2], + }), + }; + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + it('should render the selected thumbnail', () => { + const mockThumbnail1Element = fixture.debugElement.query(By.css('daff-mock-thumbnail1')); + const mockThumbnail2Element = fixture.debugElement.query(By.css('daff-mock-thumbnail2')); + expect(mockThumbnail1Element).toBeDefined(); + expect(mockThumbnail2Element).toBeNull(); + }); +}); diff --git a/libs/design/src/molecules/media-gallery/media-renderer/media-renderer.component.ts b/libs/design/src/molecules/media-gallery/media-renderer/media-renderer.component.ts new file mode 100644 index 0000000000..f8e0c08f06 --- /dev/null +++ b/libs/design/src/molecules/media-gallery/media-renderer/media-renderer.component.ts @@ -0,0 +1,107 @@ +import { + Component, + OnInit, + ComponentFactoryResolver, + Input, + Type, + ViewChild, + ViewContainerRef, + TemplateRef, + ChangeDetectionStrategy, + OnDestroy, +} from '@angular/core'; +import { + Subscription, + Subject, +} from 'rxjs'; +import { takeUntil } from 'rxjs/operators'; + +import { DaffMediaGalleryComponent } from '../media-gallery.component'; +import { DaffMediaGalleryRegistry } from '../registry/media-gallery.registry'; + +/** + * Dynamically renders the selected `DaffThumbnailDirective` in a `daff-media-gallery` any time the selected thumbnail + * changes. + */ +@Component({ + selector: 'daff-media-renderer', + templateUrl: './media-renderer.component.html', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class DaffMediaRendererComponent implements OnInit, OnDestroy { + + /** + * Private tracker for indicating when the component is destroyed. + */ + private _destroy$ = new Subject(); + + /** + * The constructor function of the component to render. + */ + @Input() component: Type; + + constructor( + private componentFactoryResolver: ComponentFactoryResolver, + private registry: DaffMediaGalleryRegistry, + private gallery: DaffMediaGalleryComponent, + ) {} + + /** + * The slot that the "component" is rendered into. + */ + @ViewChild(TemplateRef, { static: true, read: ViewContainerRef }) + slot: ViewContainerRef; + + ngOnInit() { + this.registry.galleries[this.gallery.name] + .pipe(takeUntil(this._destroy$)) + .subscribe((gallery) => { + + /** + * Clear out the slot for the dynamically rendered thumbnail + */ + this.slot.clear(); + + const _selectedThumbnail = gallery.thumbnails.filter(media => media.selected).shift(); + + /** + * If there's no selected media, render nothing. + */ + if(!_selectedThumbnail) { + return; + } + + const _selectedThumbnailComponent = _selectedThumbnail.component; + + /** + * Create the component to insert. + */ + const component = this.componentFactoryResolver.resolveComponentFactory( + >_selectedThumbnailComponent.constructor, + ); + const componentRef = this.slot.createComponent(component); + + /** + * Fill the component with it's values from the original component + */ + component.inputs.forEach(input => { + componentRef.instance[input.propName] = _selectedThumbnailComponent[input.propName]; + }); + + /** + * Trigger a change detection on the component tree, outside the cycle to address + * the fact that the component was created outside a typical CD loop. + */ + componentRef.changeDetectorRef.detectChanges(); + }); + } + + /** + * Used to unsubscribe from the dynamic component. + */ + ngOnDestroy() { + this._destroy$.next(true); + this._destroy$.unsubscribe(); + } +} + diff --git a/libs/design/src/molecules/media-gallery/public_api.ts b/libs/design/src/molecules/media-gallery/public_api.ts new file mode 100644 index 0000000000..2d801c4d29 --- /dev/null +++ b/libs/design/src/molecules/media-gallery/public_api.ts @@ -0,0 +1,5 @@ +export * from './media-gallery.component'; +export * from './media-gallery.module'; +export * from './thumbnail/thumbnail.directive'; + +export { daffThumbnailCompatToken } from './thumbnail/thumbnail-compat.token'; diff --git a/libs/design/src/molecules/media-gallery/registry/media-gallery.registry.spec.ts b/libs/design/src/molecules/media-gallery/registry/media-gallery.registry.spec.ts new file mode 100644 index 0000000000..3f9c0e2ccf --- /dev/null +++ b/libs/design/src/molecules/media-gallery/registry/media-gallery.registry.spec.ts @@ -0,0 +1,158 @@ +import { + ChangeDetectorRef, + Directive, +} from '@angular/core'; +import { + waitForAsync, + TestBed, +} from '@angular/core/testing'; +import { BehaviorSubject } from 'rxjs'; + +import { DaffMediaGalleryComponent } from '../media-gallery.component'; +import { DaffMediaGalleryRegistry } from './media-gallery.registry'; + +@Directive({ + selector: '[daffMockThumbnail]', +}) +export class DaffMockThumbnailDirective { + + constructor( + public gallery: DaffMediaGalleryComponent, + ) {} + + select = jasmine.createSpy(); + selected: boolean; +} + +describe('DaffMediaGalleryRegistry', () => { + let service: DaffMediaGalleryRegistry; + let mockGalleryAlreadyAdded; + let mockThumbnailAlreadyAdded; + + beforeEach(waitForAsync(() => { + TestBed.configureTestingModule({ + providers: [ + DaffMediaGalleryRegistry, + ChangeDetectorRef, + ], + }); + service = TestBed.inject(DaffMediaGalleryRegistry); + + mockGalleryAlreadyAdded = new DaffMediaGalleryComponent(); + mockThumbnailAlreadyAdded = new DaffMockThumbnailDirective(mockGalleryAlreadyAdded); + service.galleries = { + [mockGalleryAlreadyAdded.name]: new BehaviorSubject({ + gallery: mockGalleryAlreadyAdded, + thumbnails: [mockThumbnailAlreadyAdded], + }), + }; + })); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); + + describe('add', () => { + + describe('when the gallery given already exists in the registry', () => { + + it('should add the thumbnail to the gallery given', () => { + const newThumbnail: any = new DaffMockThumbnailDirective(mockGalleryAlreadyAdded); + service.add(mockGalleryAlreadyAdded, newThumbnail); + + expect(service.galleries[mockGalleryAlreadyAdded.name].getValue().thumbnails.findIndex((t) => (t) === newThumbnail)).toBeGreaterThan(-1); + }); + + it('should not add the thumbnail when the thumbnail already exists in the registry', () => { + expect(service.galleries[mockGalleryAlreadyAdded.name].getValue().thumbnails.length).toEqual(1); + service.add(mockGalleryAlreadyAdded, mockThumbnailAlreadyAdded); + + expect(service.galleries[mockGalleryAlreadyAdded.name].getValue().thumbnails.length).toEqual(1); + }); + }); + + describe('when the gallery given does not exist in the registry', () => { + + let newGallery; + let newThumbnail; + let addedGallery; + + beforeEach(() => { + newGallery = new DaffMediaGalleryComponent(); + newThumbnail = new DaffMockThumbnailDirective(newGallery); + + service.add(newGallery, newThumbnail); + + addedGallery = service.galleries[newGallery.name].getValue(); + }); + + it('should add the gallery to the registry', () => { + expect(addedGallery.gallery).toEqual(newGallery); + }); + + it('should add the thumbnail to the gallery given', () => { + expect(addedGallery.thumbnails.findIndex(t => t === newThumbnail)).toBeGreaterThan(-1); + }); + + it('should select the thumbnail', () => { + expect(newThumbnail.select).toHaveBeenCalled(); + }); + }); + }); + + describe('remove', () => { + + it('should not do anything if the gallery associated with the given thumbnail DNE', () => { + const newGallery = new DaffMediaGalleryComponent(); + const newThumbnail: any = new DaffMockThumbnailDirective(newGallery); + service.remove(newThumbnail); + + expect(service.galleries[mockGalleryAlreadyAdded.name].getValue().thumbnails.length).toEqual(1); + }); + + it('should not do anything if the thumbnail does not exist in the registry', () => { + const newThumbnail: any = new DaffMockThumbnailDirective(mockGalleryAlreadyAdded); + service.remove(newThumbnail); + + expect(service.galleries[mockGalleryAlreadyAdded.name].getValue().thumbnails.length).toEqual(1); + }); + + it('should remove the thumbnail from the registry', () => { + service.remove(mockThumbnailAlreadyAdded); + + expect(service.galleries[mockGalleryAlreadyAdded.name].getValue().thumbnails.length).toEqual(0); + }); + }); + + describe('select', () => { + + it('should not do anything if the gallery associated with the given thumbnail DNE', () => { + const newGallery = new DaffMediaGalleryComponent(); + const newThumbnail: any = new DaffMockThumbnailDirective(newGallery); + service.select(newThumbnail); + + expect(newThumbnail.select).not.toHaveBeenCalled(); + }); + + it('should not do anything if the thumbnail is already selected', () => { + mockThumbnailAlreadyAdded.selected = true; + service.select(mockThumbnailAlreadyAdded); + + expect(mockThumbnailAlreadyAdded.selected).toEqual(true); + }); + + it('should not do anything if the thumbnail does not exist in the registry', () => { + const newThumbnail: any = new DaffMockThumbnailDirective(mockGalleryAlreadyAdded); + service.select(newThumbnail); + + expect(newThumbnail.select).not.toHaveBeenCalled(); + }); + + it('should select the thumbnail', () => { + mockThumbnailAlreadyAdded.selected = false; + service.select(mockThumbnailAlreadyAdded); + + expect(mockThumbnailAlreadyAdded.select).toHaveBeenCalled(); + }); + }); +}); diff --git a/libs/design/src/molecules/media-gallery/registry/media-gallery.registry.ts b/libs/design/src/molecules/media-gallery/registry/media-gallery.registry.ts new file mode 100644 index 0000000000..8515cb704d --- /dev/null +++ b/libs/design/src/molecules/media-gallery/registry/media-gallery.registry.ts @@ -0,0 +1,96 @@ +import { Injectable } from '@angular/core'; +import { BehaviorSubject } from 'rxjs'; + +import { DaffMediaGalleryComponent } from '../media-gallery.component'; +import { DaffThumbnailDirective } from '../thumbnail/thumbnail.directive'; + +export interface DaffMediaGalleryDict { + [galleryName: string]: BehaviorSubject; +} + +export interface DaffMediaGallery { + gallery: DaffMediaGalleryComponent; + thumbnails: DaffThumbnailDirective[]; +} + +@Injectable({ providedIn: 'root' }) +export class DaffMediaGalleryRegistry { + galleries: DaffMediaGalleryDict = {}; + + /** + * @description + * Adds a media element to the internal registry. + */ + add(gallery: DaffMediaGalleryComponent, thumbnail: DaffThumbnailDirective) { + if(this.galleries[gallery.name]) { + let newGallery = this.galleries[gallery.name].getValue(); + + newGallery = { + ...newGallery, + thumbnails: [ + ...newGallery.thumbnails.filter(t => t !== thumbnail), + thumbnail, + ], + }; + this.galleries[gallery.name].next(newGallery); + } else { + this.galleries[gallery.name] = new BehaviorSubject({ + gallery, + thumbnails: [thumbnail], + }); + } + + if(this.galleries[gallery.name].getValue().thumbnails.length === 1) { + thumbnail.select(); + } + } + + /** + * @description + * Removes a media element from the internal registry. + */ + remove(thumbnail: DaffThumbnailDirective) { + if(!this.galleries[thumbnail.gallery.name]) { + return; + } + const gallery = this.galleries[thumbnail.gallery.name].getValue(); + const index = gallery.thumbnails.indexOf(thumbnail); + + if(index === -1) { + return; + } + + this.galleries[thumbnail.gallery.name].next({ + ...gallery, + thumbnails: [ + ...gallery.thumbnails.slice(0, index), + ...gallery.thumbnails.slice(index + 1), + ], + }); + } + + /** + * @description + * Selects a media element for a given gallery. + */ + select(thumbnail: DaffThumbnailDirective) { + if(!this.galleries[thumbnail.gallery.name]) { + return; + } + + const gallery = this.galleries[thumbnail.gallery.name].getValue(); + const index = gallery.thumbnails.indexOf(thumbnail); + + if(thumbnail.selected || index === -1){ + return; + } + + this.galleries[thumbnail.gallery.name].next({ + ...gallery, + thumbnails: [ + ...gallery.thumbnails.filter(m => m !== thumbnail).map(m => m.deselect()), + thumbnail.select(), + ], + }); + } +} diff --git a/libs/design/src/molecules/media-gallery/thumbnail/thumbnail-compat.token.ts b/libs/design/src/molecules/media-gallery/thumbnail/thumbnail-compat.token.ts new file mode 100644 index 0000000000..f74a91dfea --- /dev/null +++ b/libs/design/src/molecules/media-gallery/thumbnail/thumbnail-compat.token.ts @@ -0,0 +1,6 @@ +import { InjectionToken } from '@angular/core'; + +/** + * A multi provider injection token that marks a component as renderable for the `DaffMediaRendererComponent`. + */ +export const daffThumbnailCompatToken = new InjectionToken('thumbnailCompatToken'); diff --git a/libs/design/src/molecules/media-gallery/thumbnail/thumbnail.directive.spec.ts b/libs/design/src/molecules/media-gallery/thumbnail/thumbnail.directive.spec.ts new file mode 100644 index 0000000000..6683008a9c --- /dev/null +++ b/libs/design/src/molecules/media-gallery/thumbnail/thumbnail.directive.spec.ts @@ -0,0 +1,142 @@ +import { + Component, + DebugElement, +} from '@angular/core'; +import { + ComponentFixture, + TestBed, + waitForAsync, +} from '@angular/core/testing'; +import { By } from '@angular/platform-browser'; + +import { DaffMediaGalleryComponent } from '../media-gallery.component'; +import { DaffMediaGalleryRegistry } from '../registry/media-gallery.registry'; +import { daffThumbnailCompatToken } from './thumbnail-compat.token'; +import { DaffThumbnailDirective } from './thumbnail.directive'; + +@Component({ + template: `
`, +}) +class WrapperComponent { + becameSelectedFunction() {}; +} + +@Component({ + template: '', + selector: 'daff-media-renderer', +}) +class MockMediaRendererComponent {} + +describe('DaffThumbnailDirective', () => { + let wrapper: WrapperComponent; + let de: DebugElement; + let directive: DaffThumbnailDirective; + let fixture: ComponentFixture; + let registry: DaffMediaGalleryRegistry; + + beforeEach(waitForAsync(() => { + TestBed.configureTestingModule({ + declarations: [ + DaffThumbnailDirective, + DaffMediaGalleryComponent, + WrapperComponent, + MockMediaRendererComponent, + ], + providers: [ + { + provide: DaffMediaGalleryRegistry, + useValue: jasmine.createSpyObj('DaffMediaGalleryRegistry', ['add', 'remove', 'select']), + }, + { provide: daffThumbnailCompatToken, useValue: DaffThumbnailDirective }, + ], + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(WrapperComponent); + registry = TestBed.inject(DaffMediaGalleryRegistry); + wrapper = fixture.componentInstance; + de = fixture.debugElement.query(By.css('[daffThumbnail]')); + directive = fixture.debugElement.query(By.directive(DaffThumbnailDirective)).injector.get(DaffThumbnailDirective); + fixture.detectChanges(); + }); + + it('should create', () => { + expect(wrapper).toBeTruthy(); + }); + + it('should add a class of "daff-thumbnail" to the host element', () => { + expect(de.classes).toEqual(jasmine.objectContaining({ + 'daff-thumbnail': true, + })); + }); + + it('should add a "daff-thumbnail--selected" class when the thumbnail is selected', () => { + directive.selected = true; + fixture.detectChanges(); + + expect(de.classes).toEqual(jasmine.objectContaining({ + 'daff-thumbnail--selected': true, + })); + }); + + it('should add itself to the media-gallery registry on initialization', () => { + expect(registry.add).toHaveBeenCalledWith(directive.gallery, directive); + }); + + it('should notify the registry when the thumbnail is clicked', () => { + de.nativeElement.click(); + fixture.detectChanges(); + + expect(registry.select).toHaveBeenCalledWith(directive); + }); + + it('should remove itself from the registry when it is destroyed', () => { + directive.ngOnDestroy(); + + expect(registry.remove).toHaveBeenCalledWith(directive); + }); + + describe('select', () => { + + let result; + + beforeEach(() => { + spyOn(wrapper, 'becameSelectedFunction'); + directive.selected = false; + result = directive.select(); + }); + + it('should set the thumbnail as selected', () => { + expect(directive.selected).toEqual(true); + }); + + it('should notify that it became selected', () => { + expect(wrapper.becameSelectedFunction).toHaveBeenCalled(); + }); + + it('should return itself', () => { + expect(result).toEqual(directive); + }); + }); + + describe('deselect', () => { + + let result; + + beforeEach(() => { + spyOn(wrapper, 'becameSelectedFunction'); + directive.selected = true; + result = directive.deselect(); + }); + + it('should set the thumbnail as unselected', () => { + expect(directive.selected).toEqual(false); + }); + + it('should return itself', () => { + expect(result).toEqual(directive); + }); + }); +}); diff --git a/libs/design/src/molecules/media-gallery/thumbnail/thumbnail.directive.ts b/libs/design/src/molecules/media-gallery/thumbnail/thumbnail.directive.ts new file mode 100644 index 0000000000..9eb6323e34 --- /dev/null +++ b/libs/design/src/molecules/media-gallery/thumbnail/thumbnail.directive.ts @@ -0,0 +1,86 @@ +import { + Directive, + Input, + Inject, + Type, + HostBinding, + HostListener, + Output, + EventEmitter, + ChangeDetectorRef, + OnInit, + OnDestroy, +} from '@angular/core'; + +import { DaffMediaGalleryComponent } from '../media-gallery.component'; +import { DaffMediaGalleryRegistry } from '../registry/media-gallery.registry'; +import { daffThumbnailCompatToken } from './thumbnail-compat.token'; + +/** + * A directive marking thumbnails for the `DaffMediaRendererComponent`. Needs to be wrapped in a `daff-media-gallery` component + * and needs to be placed on a component that is provided as a `daffThumbnailCompatToken`. + */ +@Directive({ + selector: '[daffThumbnail]', +}) +export class DaffThumbnailDirective implements OnInit, OnDestroy { + + /** + * Adds a class for styling a selected thumbnail + */ + @HostBinding('class.daff-thumbnail--selected') get selectedClass() { + return this.selected; + }; + + constructor( + @Inject(daffThumbnailCompatToken) public component: Type, + private cd: ChangeDetectorRef, + private registry: DaffMediaGalleryRegistry, + public gallery: DaffMediaGalleryComponent, + ) {} + + /** + * Adds a class for styling a thumbnail + */ + @HostBinding('class.daff-thumbnail') class = true; + + /** + * A prop for determining whether or not the media element is selected. + */ + selected = false; + + /** + * An event that fires after the media element becomes selected. + */ + @Output() becameSelected: EventEmitter = new EventEmitter(); + + /** + * Adds a click event to trigger selection of the media element. + * + * @param $event + */ + @HostListener('click', ['$event']) onClick($event: MouseEvent) { + this.registry.select(this); + } + + ngOnInit(): void { + this.registry.add(this.gallery, this); + } + + ngOnDestroy(): void { + this.registry.remove(this); + } + + select() { + this.selected = true; + this.becameSelected.emit(); + this.cd.markForCheck(); + return this; + } + + deselect() { + this.selected = false; + this.cd.markForCheck(); + return this; + } +} diff --git a/libs/design/src/public_api.ts b/libs/design/src/public_api.ts index 8f9132a0e6..0aa9bed7ea 100644 --- a/libs/design/src/public_api.ts +++ b/libs/design/src/public_api.ts @@ -28,6 +28,7 @@ export * from './molecules/image-gallery/public_api'; export * from './molecules/image-list/public_api'; export * from './molecules/link-set/public_api'; export * from './molecules/list/public_api'; +export * from './molecules/media-gallery/public_api'; export * from './molecules/modal/public_api'; export * from './molecules/navbar/public_api'; export * from './molecules/paginator/public_api'; diff --git a/libs/design/src/scss/theming/_theme.scss b/libs/design/src/scss/theming/_theme.scss index c6604d2251..8e7525f1d6 100644 --- a/libs/design/src/scss/theming/_theme.scss +++ b/libs/design/src/scss/theming/_theme.scss @@ -12,6 +12,7 @@ @import '../../molecules/card/card/card-theme'; @import '../../molecules/hero/hero-theme'; @import '../../molecules/list/list-theme'; +@import '../../molecules/media-gallery/media-gallery-theme'; @import '../../molecules/modal/modal-theme'; @import '../../molecules/navbar/navbar-theme'; @import '../../molecules/paginator/paginator-theme'; @@ -42,6 +43,7 @@ @include daff-card-theme($theme); @include daff-hero-theme($theme); @include daff-list-theme($theme); + @include daff-media-gallery-theme($theme); @include daff-modal-theme($theme); @include daff-navbar-theme($theme); @include daff-paginator-theme($theme);