Skip to content

Commit

Permalink
feat(design): create DaffSelectable host directive (#2910)
Browse files Browse the repository at this point in the history
  • Loading branch information
xelaint authored Dec 17, 2024
1 parent 8879733 commit 9245bb6
Show file tree
Hide file tree
Showing 14 changed files with 187 additions and 79 deletions.
4 changes: 2 additions & 2 deletions libs/design/media-gallery/src/media-gallery-theme.scss
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,14 @@
.daff-thumbnail {
border: 1px solid transparent;

&--selected {
&.daff-selected {
border: 1px solid theming.daff-color($neutral);
}
}

&.daff-skeleton {
.daff-thumbnail {
&--selected {
&.daff-selected {
border: none;
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
import { By } from '@angular/platform-browser';
import { BehaviorSubject } from 'rxjs';

import { DaffSelectableDirective } from '@daffodil/design';
import { DaffArticleComponent } from '@daffodil/design/article';

import { DaffMediaRendererComponent } from './media-renderer.component';
Expand Down Expand Up @@ -82,11 +83,11 @@ describe('@daffodil/design/media-gallery | DaffMediaRendererComponent', () => {
fixture = TestBed.createComponent(DaffMediaRendererComponent);
registry = TestBed.inject(DaffMediaGalleryRegistry);

mockThumbnail1 = new DaffThumbnailDirective(null, jasmine.createSpyObj('ChangeDetectorRef', ['markForCheck']), null, stubRegistration);
mockThumbnail1 = new DaffThumbnailDirective(null, null, stubRegistration, new DaffSelectableDirective(jasmine.createSpyObj('ChangeDetectorRef', ['markForCheck'])));
mockThumbnail1.component = <Type<unknown>><unknown>(new DaffArticleComponent());
mockThumbnail2 = new DaffThumbnailDirective(null, jasmine.createSpyObj('ChangeDetectorRef', ['markForCheck']), null, stubRegistration);
mockThumbnail1.selected = true;
mockThumbnail2.selected = false;
mockThumbnail2 = new DaffThumbnailDirective(null, null, stubRegistration, new DaffSelectableDirective(jasmine.createSpyObj('ChangeDetectorRef', ['markForCheck'])));
mockThumbnail1.select();
mockThumbnail2.deselect();
registry.galleries = {
[stubRegistration.name]: new BehaviorSubject({
gallery: stubRegistration,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import {
} from '@angular/core/testing';
import { BehaviorSubject } from 'rxjs';

import { DaffSelectableDirective } from '@daffodil/design';

import {
DaffMediaGallery,
DaffMediaGalleryRegistry,
Expand Down Expand Up @@ -42,7 +44,7 @@ describe('@daffodil/design/media-gallery | DaffMediaGalleryRegistry', () => {
});
registry = TestBed.inject(DaffMediaGalleryRegistry);
cd = TestBed.inject(ChangeDetectorRef);
mockThumbnailAlreadyAdded = new DaffThumbnailDirective(FakeComponent, cd, registry, mockGalleryAlreadyAdded);
mockThumbnailAlreadyAdded = new DaffThumbnailDirective(FakeComponent, registry, mockGalleryAlreadyAdded, new DaffSelectableDirective(cd));

registry.galleries = {
[mockGalleryAlreadyAdded.name]: new BehaviorSubject({
Expand All @@ -61,7 +63,7 @@ describe('@daffodil/design/media-gallery | DaffMediaGalleryRegistry', () => {
describe('when the gallery given already exists in the registry', () => {

it('should add the thumbnail to the gallery given', () => {
const newThumbnail: any = new DaffThumbnailDirective(FakeComponent, cd, registry, mockGalleryAlreadyAdded);
const newThumbnail: any = new DaffThumbnailDirective(FakeComponent, registry, mockGalleryAlreadyAdded, new DaffSelectableDirective(cd));
registry.add(mockGalleryAlreadyAdded, newThumbnail);

expect(registry.galleries[mockGalleryAlreadyAdded.name].getValue().thumbnails.findIndex((t) =>
Expand All @@ -86,7 +88,7 @@ describe('@daffodil/design/media-gallery | DaffMediaGalleryRegistry', () => {
let addedGallery: DaffMediaGallery;

beforeEach(() => {
newThumbnail = new DaffThumbnailDirective(FakeComponent, cd, registry, newGallery);
newThumbnail = new DaffThumbnailDirective(FakeComponent, registry, newGallery, new DaffSelectableDirective(cd));

spyOn(newThumbnail, 'select').and.callThrough();

Expand Down Expand Up @@ -114,14 +116,14 @@ describe('@daffodil/design/media-gallery | DaffMediaGalleryRegistry', () => {
const newGallery: DaffMediaGalleryRegistration = {
name: 'newGallery',
};
const newThumbnail = new DaffThumbnailDirective(FakeComponent, cd, registry, newGallery);
const newThumbnail = new DaffThumbnailDirective(FakeComponent, registry, newGallery, new DaffSelectableDirective(cd));
registry.remove(newThumbnail);

expect(registry.galleries[mockGalleryAlreadyAdded.name].getValue().thumbnails.length).toEqual(1);
});

it('should not do anything if the thumbnail does not exist in the registry', () => {
const newThumbnail = new DaffThumbnailDirective(FakeComponent, cd, registry, mockGalleryAlreadyAdded);
const newThumbnail = new DaffThumbnailDirective(FakeComponent, registry, mockGalleryAlreadyAdded, new DaffSelectableDirective(cd));
registry.remove(newThumbnail);

expect(registry.galleries[mockGalleryAlreadyAdded.name].getValue().thumbnails.length).toEqual(1);
Expand All @@ -143,30 +145,29 @@ describe('@daffodil/design/media-gallery | DaffMediaGalleryRegistry', () => {
describe('select', () => {

it('should not do anything if the gallery associated with the given thumbnail DNE', () => {
const newThumbnail = new DaffThumbnailDirective(FakeComponent, cd, registry, mockGalleryAlreadyAdded);
const newThumbnail = new DaffThumbnailDirective(FakeComponent, registry, mockGalleryAlreadyAdded, new DaffSelectableDirective(cd));
spyOn(newThumbnail, 'select').and.callThrough();
registry.select(newThumbnail);

expect(newThumbnail.select).not.toHaveBeenCalled();
});

it('should not do anything if the thumbnail is already selected', () => {
mockThumbnailAlreadyAdded.selected = true;
mockThumbnailAlreadyAdded.select();
registry.select(mockThumbnailAlreadyAdded);

expect(mockThumbnailAlreadyAdded.selected).toEqual(true);
});

it('should not do anything if the thumbnail does not exist in the registry', () => {
const newThumbnail = new DaffThumbnailDirective(FakeComponent, cd, registry, mockGalleryAlreadyAdded);
const newThumbnail = new DaffThumbnailDirective(FakeComponent, registry, mockGalleryAlreadyAdded, new DaffSelectableDirective(cd));
spyOn(newThumbnail, 'select').and.callThrough();
registry.select(newThumbnail);

expect(newThumbnail.select).not.toHaveBeenCalled();
});

it('should select the thumbnail', () => {
mockThumbnailAlreadyAdded.selected = false;
spyOn(mockThumbnailAlreadyAdded, 'select').and.callThrough();

registry.select(mockThumbnailAlreadyAdded);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -83,15 +83,6 @@ describe('@daffodil/design/media-gallery | DaffThumbnailDirective', () => {
}));
});

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);
});
Expand All @@ -115,7 +106,7 @@ describe('@daffodil/design/media-gallery | DaffThumbnailDirective', () => {

beforeEach(() => {
spyOn(wrapper, 'becameSelectedFunction');
directive.selected = false;
directive.select();
result = directive.select();
});

Expand All @@ -138,7 +129,7 @@ describe('@daffodil/design/media-gallery | DaffThumbnailDirective', () => {

beforeEach(() => {
spyOn(wrapper, 'becameSelectedFunction');
directive.selected = true;
directive.select();
result = directive.deselect();
});

Expand Down
57 changes: 22 additions & 35 deletions libs/design/media-gallery/src/thumbnail/thumbnail.directive.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,12 @@ import {
Type,
HostBinding,
HostListener,
Output,
EventEmitter,
ChangeDetectorRef,
OnInit,
OnDestroy,
} from '@angular/core';

import { DaffSelectableDirective } from '@daffodil/design';

import { daffThumbnailCompatToken } from './thumbnail-compat.token';
import { DaffThumbnailRegistration } from './thumbnail-registration.interface';
import { DaffMediaGalleryRegistration } from '../helpers/media-gallery-registration.interface';
Expand All @@ -24,37 +23,38 @@ import { DaffMediaGalleryRegistry } from '../registry/media-gallery.registry';
@Directive({
selector: '[daffThumbnail]',
standalone: true,
hostDirectives: [{
directive: DaffSelectableDirective,
inputs: ['selected'],
outputs: ['becameSelected'],
}],
})
export class DaffThumbnailDirective implements OnInit, OnDestroy, DaffThumbnailRegistration {

/**
* Adds a class for styling a selected thumbnail
*/
@HostBinding('class.daff-thumbnail--selected') get selectedClass() {
return this.selected;
};

constructor(
@Inject(daffThumbnailCompatToken) public component: Type<unknown>,
private cd: ChangeDetectorRef,
private registry: DaffMediaGalleryRegistry,
@Inject(DAFF_MEDIA_GALLERY_TOKEN) public gallery: DaffMediaGalleryRegistration,
private selectedDirective: DaffSelectableDirective,
) {}

/**
* Adds a class for styling a thumbnail
*/
@HostBinding('class.daff-thumbnail') class = true;
public get selected() {
return this.selectedDirective.selected;
}

/**
* A prop for determining whether or not the media element is selected.
*/
selected = false;
public select() {
this.selectedDirective.select();
return this;
}

public deselect() {
this.selectedDirective.deselect();
return this;
}

/**
* An event that fires after the media element becomes selected.
* Adds a class for styling a thumbnail
*/
@Output() becameSelected: EventEmitter<void> = new EventEmitter<void>();
@HostBinding('class.daff-thumbnail') class = true;

/**
* Adds a click event to trigger selection of the media element.
Expand All @@ -72,17 +72,4 @@ export class DaffThumbnailDirective implements OnInit, OnDestroy, DaffThumbnailR
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;
}
}
1 change: 1 addition & 0 deletions libs/design/src/core/public_api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,4 @@ export * from './lazy/public_api';
export * from './focus/public_api';
export * from './sizable/public_api';
export * from './openable/public_api';
export * from './selectable/public_api';
2 changes: 2 additions & 0 deletions libs/design/src/core/selectable/public_api.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export { DaffSelectable } from './selectable';
export { DaffSelectableDirective } from './selectable.directive';
78 changes: 78 additions & 0 deletions libs/design/src/core/selectable/selectable.directive.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import {
Component,
DebugElement,
} from '@angular/core';
import {
waitForAsync,
ComponentFixture,
TestBed,
} from '@angular/core/testing';
import { By } from '@angular/platform-browser';

import { DaffSelectableDirective } from './selectable.directive';

@Component({
template: `
<div daffSelected
(becameSelected)="becameSelectedFunction($event)"
[selected]="selected">
</div>`,
})

class WrapperComponent {
becameSelected = (val: boolean) => {};
selected: boolean;
}

describe('@daffodil/design | DaffSelectableDirective', () => {
let wrapper: WrapperComponent;
let de: DebugElement;
let fixture: ComponentFixture<WrapperComponent>;
let directive: DaffSelectableDirective;

beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
declarations: [
WrapperComponent,
],
imports: [
DaffSelectableDirective,
],
})
.compileComponents();
}));

beforeEach(() => {
fixture = TestBed.createComponent(WrapperComponent);
wrapper = fixture.componentInstance;
de = fixture.debugElement.query(By.css('[daffSelected]'));
directive = de.injector.get(DaffSelectableDirective);
fixture.detectChanges();
});

it('should create', () => {
expect(wrapper).toBeTruthy();
expect(directive).toBeTruthy();
});

it('should add a class of "daff-selected" to the host element when selected is true', () => {
wrapper.selected = true;
fixture.detectChanges();

expect(de.classes).toEqual(jasmine.objectContaining({
'daff-selected': true,
}));
});

it('should not add a class of "daff-selected" to the host element when selected is false', () => {
expect(de.classes['daff-selected']).toBeUndefined();
});

it('should emit on becameSelected when select is called', () => {
spyOn(directive.becameSelected, 'emit');
wrapper.selected = true;
directive.select();

expect(directive.becameSelected.emit).toHaveBeenCalledWith();
});
});
40 changes: 40 additions & 0 deletions libs/design/src/core/selectable/selectable.directive.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import {
ChangeDetectorRef,
Directive,
EventEmitter,
HostBinding,
Input,
Output,
} from '@angular/core';

import { DaffSelectable } from '../selectable/selectable';

@Directive({
selector: '[daffSelected]',
standalone: true,
})

export class DaffSelectableDirective implements DaffSelectable {
/** Whether or not a component implementing the directive is selected */
@Input() @HostBinding('class.daff-selected') selected = false;

/**
* An event that fires after the media element becomes selected.
*/
@Output() becameSelected: EventEmitter<void> = new EventEmitter<void>();

constructor(private cd: ChangeDetectorRef) {}

select() {
this.selected = true;
this.becameSelected.emit();
this.cd.markForCheck();
return this;
}

deselect() {
this.selected = false;
this.cd.markForCheck();
return this;
}
}
7 changes: 7 additions & 0 deletions libs/design/src/core/selectable/selectable.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
/**
* An interface for giving a component the ability to display a selected UI.
* In order to be selectable, the class must implement this property.
*/
export interface DaffSelectable {
selected: boolean;
}
2 changes: 1 addition & 1 deletion libs/design/tabs/src/tabs-theme.scss
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
.daff-tab-activator {
border-bottom: 2px solid theming.daff-illuminate($base, $neutral, 2);

&.selected { /* stylelint-disable-line selector-class-pattern */
&.daff-selected {
border-bottom: 2px solid theming.daff-color($primary);
}
}
Expand Down
Loading

0 comments on commit 9245bb6

Please sign in to comment.