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.