From 7857b1c105cb7059b87273d8c0658a989ffcf270 Mon Sep 17 00:00:00 2001 From: Matheus Davidson Date: Wed, 18 May 2022 20:48:12 -0300 Subject: [PATCH] feat(dropdown): opening and closing with trigger --- package.json | 6 +- .../dropdown/dropdown-item.component.css | 0 .../dropdown/dropdown-item.component.html | 1 + .../dropdown/dropdown-item.component.ts | 82 ++++++++++++++++ .../dropdown/dropdown-panel.interface.ts | 11 +++ .../dropdown-trigger-for.directive.ts | 95 +++++++++++++++++++ .../modules/dropdown/dropdown.component.css | 0 .../modules/dropdown/dropdown.component.html | 14 +++ .../dropdown/dropdown.component.spec.ts | 25 +++++ .../modules/dropdown/dropdown.component.ts | 40 ++++++++ .../src/modules/dropdown/dropdown.module.ts | 13 +++ projects/ng-tw/src/public-api.ts | 6 ++ .../introduction-route.component.html | 15 +++ .../introduction-route.component.ts | 4 + .../introduction-route.module.ts | 3 +- 15 files changed, 311 insertions(+), 4 deletions(-) create mode 100644 projects/ng-tw/src/modules/dropdown/dropdown-item.component.css create mode 100644 projects/ng-tw/src/modules/dropdown/dropdown-item.component.html create mode 100644 projects/ng-tw/src/modules/dropdown/dropdown-item.component.ts create mode 100644 projects/ng-tw/src/modules/dropdown/dropdown-panel.interface.ts create mode 100644 projects/ng-tw/src/modules/dropdown/dropdown-trigger-for.directive.ts create mode 100644 projects/ng-tw/src/modules/dropdown/dropdown.component.css create mode 100644 projects/ng-tw/src/modules/dropdown/dropdown.component.html create mode 100644 projects/ng-tw/src/modules/dropdown/dropdown.component.spec.ts create mode 100644 projects/ng-tw/src/modules/dropdown/dropdown.component.ts create mode 100644 projects/ng-tw/src/modules/dropdown/dropdown.module.ts diff --git a/package.json b/package.json index 507f1d5..ccc6977 100644 --- a/package.json +++ b/package.json @@ -3,9 +3,9 @@ "version": "0.0.2", "scripts": { "ng": "ng", - "start": "ng serve", - "build": "ng build", - "watch": "ng build --watch --configuration development", + "start": "ng serve --proejct ng-tw", + "build": "ng build --proejct ng-tw", + "watch": "ng build --project ng-tw --watch --configuration development", "test": "ng test", "release:major": "cd ./scripts && npm run release:major", "release:minor": "cd ./scripts && npm run release:minor", diff --git a/projects/ng-tw/src/modules/dropdown/dropdown-item.component.css b/projects/ng-tw/src/modules/dropdown/dropdown-item.component.css new file mode 100644 index 0000000..e69de29 diff --git a/projects/ng-tw/src/modules/dropdown/dropdown-item.component.html b/projects/ng-tw/src/modules/dropdown/dropdown-item.component.html new file mode 100644 index 0000000..95a0b70 --- /dev/null +++ b/projects/ng-tw/src/modules/dropdown/dropdown-item.component.html @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/projects/ng-tw/src/modules/dropdown/dropdown-item.component.ts b/projects/ng-tw/src/modules/dropdown/dropdown-item.component.ts new file mode 100644 index 0000000..6af5ca9 --- /dev/null +++ b/projects/ng-tw/src/modules/dropdown/dropdown-item.component.ts @@ -0,0 +1,82 @@ +import { Component, ChangeDetectionStrategy, Input, ElementRef } from '@angular/core'; + +/** + * IDs need to be unique across components, so this counter exists outside of + * the component definition. + */ +let _uniqueIdCounter = 0; + +@Component({ + selector: 'tw-dropdown-item, [tw-dropdown-item]', + templateUrl: './dropdown-item.component.html', + styleUrls: ['./dropdown-item.component.css'], + changeDetection: ChangeDetectionStrategy.OnPush, + host: { + '[attr.id]': 'id', + '[attr.tabindex]': '_getTabIndex()', + '[attr.aria-disabled]': 'disabled.toString()', + '[attr.disabled]': 'disabled || null', + '[attr.role]': 'menuitem', + '[class]': 'itemClass + " " + (selected === true ? activeClass : inactiveClass)', + }, +}) +export class DropdownItemComponent { + @Input() public disabled: boolean = false; + @Input() public id: string = `tw-dropdown-item-${_uniqueIdCounter++}`; + + public active: boolean = false; + public selected: boolean = false; + + public itemClass: string = 'block px-4 py-2 text-sm text-gray-700 active:bg-gray-100 active:text-gray-900'; + public activeClass: string = ''; //'bg-gray-100 text-gray-900'; + public inactiveClass: string = ''; //'text-gray-700'; + + constructor(private readonly element: ElementRef) {} + + /** + * This method sets display styles on the option to make it appear + * active. This is used by the ActiveDescendantKeyManager so key + * events will display the proper options as active on arrow key events. + */ + setActiveStyles(): void { + console.log('here'); + if (!this.active) { + this.active = true; + this.scrollIntoView(); + } + } + + /** + * This method removes display styles on the option that made it appear + * active. This is used by the ActiveDescendantKeyManager so key + * events will display the proper options as active on arrow key events. + */ + setInactiveStyles(): void { + if (this.active) { + this.active = false; + } + } + + /** Gets the label to be used when determining whether the option should be focused. */ + getLabel(): string { + return this.element.nativeElement.textContent ? this.element.nativeElement.textContent : ''; + } + + scrollIntoView() { + if (typeof this.element.nativeElement.scrollIntoView !== 'undefined') this.element.nativeElement.scrollIntoView(); + } + + select(): void { + console.log('select'); + // + // Validate disabled + if (this.disabled === true) return; + // set selected + this.selected = true; + } + + /** Used to set the `tabindex`. */ + _getTabIndex(): string { + return this.disabled ? '-1' : '0'; + } +} diff --git a/projects/ng-tw/src/modules/dropdown/dropdown-panel.interface.ts b/projects/ng-tw/src/modules/dropdown/dropdown-panel.interface.ts new file mode 100644 index 0000000..c08a330 --- /dev/null +++ b/projects/ng-tw/src/modules/dropdown/dropdown-panel.interface.ts @@ -0,0 +1,11 @@ +import { EventEmitter, QueryList, TemplateRef } from '@angular/core'; +import { DropdownItemComponent } from './dropdown-item.component'; + +export interface TwDropdownPanel { + templateRef: TemplateRef; + items: QueryList; + readonly closed: EventEmitter; + readonly yPosition: 'top' | 'bottom'; + readonly xPosition: 'start' | 'end'; + readonly id: string; +} diff --git a/projects/ng-tw/src/modules/dropdown/dropdown-trigger-for.directive.ts b/projects/ng-tw/src/modules/dropdown/dropdown-trigger-for.directive.ts new file mode 100644 index 0000000..2dac318 --- /dev/null +++ b/projects/ng-tw/src/modules/dropdown/dropdown-trigger-for.directive.ts @@ -0,0 +1,95 @@ +import { OverlayRef, Overlay } from '@angular/cdk/overlay'; +import { TemplatePortal } from '@angular/cdk/portal'; +import { Directive, OnDestroy, Input, ElementRef, ViewContainerRef, AfterContentInit, ContentChildren, QueryList } from '@angular/core'; +import { Subscription, Observable, merge } from 'rxjs'; +import { TwDropdownPanel } from './dropdown-panel.interface'; + +@Directive({ + selector: '[twDropdownTriggerFor]', + host: { + '(click)': 'toggleDropdown()', + // '(keydown)': 'handleKeydown($event)', + }, +}) +export class TwDropdownTriggerFor implements OnDestroy, AfterContentInit { + @Input('twDropdownTriggerFor') public dropdownPanel!: TwDropdownPanel; + + private isDropdownOpen = false; + private overlayRef!: OverlayRef; + private dropdownClosingActionsSub = Subscription.EMPTY; + + constructor(private overlay: Overlay, private elementRef: ElementRef, private viewContainerRef: ViewContainerRef) {} + + ngAfterContentInit(): void {} + + toggleDropdown(): void { + this.isDropdownOpen ? this.destroyDropdown() : this.openDropdown(); + } + + openDropdown(): void { + // + // Set position + const position: any = { + originX: 'start', + originY: 'bottom', + overlayX: 'start', + overlayY: 'top', + offsetY: 0, + }; + + const xPosition: any = this.dropdownPanel.xPosition === 'end' ? ['end', 'end'] : ['start', 'start']; + const yPosition: any = this.dropdownPanel.yPosition === 'top' ? ['top', 'bottom'] : ['bottom', 'top']; + + // + // Set user position + position.originX = xPosition[0]; + position.originY = yPosition[0]; + position.overlayX = xPosition[1]; + position.overlayY = yPosition[1]; + + // + // Set dropdown open + this.isDropdownOpen = true; + // create overlay + this.overlayRef = this.overlay.create({ + hasBackdrop: true, + backdropClass: 'cdk-overlay-transparent-backdrop', + scrollStrategy: this.overlay.scrollStrategies.close(), + positionStrategy: this.overlay + .position() + .flexibleConnectedTo(this.elementRef) + .withLockedPosition() + .withGrowAfterOpen() + .withPositions([position]), + }); + + const templatePortal = new TemplatePortal(this.dropdownPanel.templateRef, this.viewContainerRef); + this.overlayRef.attach(templatePortal); + + this.dropdownClosingActionsSub = this.dropdownClosingActions().subscribe(() => this.destroyDropdown()); + } + + private dropdownClosingActions(): Observable { + const backdropClick$ = this.overlayRef.backdropClick(); + const detachment$ = this.overlayRef.detachments(); + const dropdownClosed = this.dropdownPanel.closed; + + return merge(backdropClick$, detachment$, dropdownClosed); + } + + private destroyDropdown(): void { + if (!this.overlayRef || !this.isDropdownOpen) { + return; + } + + this.dropdownClosingActionsSub.unsubscribe(); + this.isDropdownOpen = false; + this.overlayRef.detach(); + } + + ngOnDestroy(): void { + if (this.overlayRef) { + this.overlayRef.dispose(); + } + } +} diff --git a/projects/ng-tw/src/modules/dropdown/dropdown.component.css b/projects/ng-tw/src/modules/dropdown/dropdown.component.css new file mode 100644 index 0000000..e69de29 diff --git a/projects/ng-tw/src/modules/dropdown/dropdown.component.html b/projects/ng-tw/src/modules/dropdown/dropdown.component.html new file mode 100644 index 0000000..fdce9f4 --- /dev/null +++ b/projects/ng-tw/src/modules/dropdown/dropdown.component.html @@ -0,0 +1,14 @@ + + + \ No newline at end of file diff --git a/projects/ng-tw/src/modules/dropdown/dropdown.component.spec.ts b/projects/ng-tw/src/modules/dropdown/dropdown.component.spec.ts new file mode 100644 index 0000000..8fde5e0 --- /dev/null +++ b/projects/ng-tw/src/modules/dropdown/dropdown.component.spec.ts @@ -0,0 +1,25 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { DropdownComponent } from './dropdown.component'; + +describe('DropdownComponent', () => { + let component: DropdownComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ DropdownComponent ] + }) + .compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(DropdownComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/projects/ng-tw/src/modules/dropdown/dropdown.component.ts b/projects/ng-tw/src/modules/dropdown/dropdown.component.ts new file mode 100644 index 0000000..85b951b --- /dev/null +++ b/projects/ng-tw/src/modules/dropdown/dropdown.component.ts @@ -0,0 +1,40 @@ +import { + Component, + ChangeDetectionStrategy, + ViewChild, + TemplateRef, + Output, + EventEmitter, + Input, + ContentChildren, + QueryList, +} from '@angular/core'; +import { DropdownItemComponent } from './dropdown-item.component'; +import { TwDropdownPanel } from './dropdown-panel.interface'; + +/** + * IDs need to be unique across components, so this counter exists outside of + * the component definition. + */ +let _uniqueIdCounter = 0; + +@Component({ + selector: 'tw-dropdown', + templateUrl: './dropdown.component.html', + styleUrls: ['./dropdown.component.css'], + changeDetection: ChangeDetectionStrategy.OnPush, + host: { + '[attr.id]': 'id', + }, +}) +export class DropdownComponent implements TwDropdownPanel { + @ViewChild(TemplateRef) public templateRef!: TemplateRef; + @ContentChildren(DropdownItemComponent, { descendants: true }) public items!: QueryList; + + @Output() public closed = new EventEmitter(); + + @Input() public yPosition: 'top' | 'bottom' = 'bottom'; + @Input() public xPosition: 'start' | 'end' = 'start'; + + @Input() public id: string = `tw-dropdown-${_uniqueIdCounter++}`; +} diff --git a/projects/ng-tw/src/modules/dropdown/dropdown.module.ts b/projects/ng-tw/src/modules/dropdown/dropdown.module.ts new file mode 100644 index 0000000..876a5e6 --- /dev/null +++ b/projects/ng-tw/src/modules/dropdown/dropdown.module.ts @@ -0,0 +1,13 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { DropdownComponent } from './dropdown.component'; +import { OverlayModule } from '@angular/cdk/overlay'; +import { TwDropdownTriggerFor } from './dropdown-trigger-for.directive'; +import { DropdownItemComponent } from './dropdown-item.component'; + +@NgModule({ + declarations: [DropdownComponent, TwDropdownTriggerFor, DropdownItemComponent], + imports: [CommonModule, OverlayModule], + exports: [DropdownComponent, TwDropdownTriggerFor, DropdownItemComponent], +}) +export class TwDropdownModule {} diff --git a/projects/ng-tw/src/public-api.ts b/projects/ng-tw/src/public-api.ts index 4e65f5c..0bad1a3 100644 --- a/projects/ng-tw/src/public-api.ts +++ b/projects/ng-tw/src/public-api.ts @@ -19,3 +19,9 @@ export * from './modules/notification/notification.service'; export * from './modules/progress-bar/progress-bar.component'; export * from './modules/progress-bar/progress-bar.module'; + +export * from './modules/dropdown/dropdown-panel.interface'; +export * from './modules/dropdown/dropdown-trigger-for.directive'; +export * from './modules/dropdown/dropdown-item.component'; +export * from './modules/dropdown/dropdown.component'; +export * from './modules/dropdown/dropdown.module'; diff --git a/projects/sandbox/src/app/routes/introduction-route/introduction-route.component.html b/projects/sandbox/src/app/routes/introduction-route/introduction-route.component.html index 664fd48..12d127d 100644 --- a/projects/sandbox/src/app/routes/introduction-route/introduction-route.component.html +++ b/projects/sandbox/src/app/routes/introduction-route/introduction-route.component.html @@ -7,6 +7,20 @@

ng-tw is a package to help you build a complete web application with Angular and Tailwind CSS. It's an implementation of tailwind components that requires javascript to run, the logic of these components runs on angular.

+ + + +
teste
+
teste 2
+
teste 4
+
+ +

Components

@@ -15,6 +29,7 @@