Skip to content

Commit

Permalink
feat(dropdown): opening and closing with trigger
Browse files Browse the repository at this point in the history
  • Loading branch information
matheusdavidson committed May 18, 2022
1 parent 4979851 commit 7857b1c
Show file tree
Hide file tree
Showing 15 changed files with 311 additions and 4 deletions.
6 changes: 3 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
Empty file.
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<ng-content></ng-content>
82 changes: 82 additions & 0 deletions projects/ng-tw/src/modules/dropdown/dropdown-item.component.ts
Original file line number Diff line number Diff line change
@@ -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<HTMLElement>) {}

/**
* 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';
}
}
11 changes: 11 additions & 0 deletions projects/ng-tw/src/modules/dropdown/dropdown-panel.interface.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { EventEmitter, QueryList, TemplateRef } from '@angular/core';
import { DropdownItemComponent } from './dropdown-item.component';

export interface TwDropdownPanel {
templateRef: TemplateRef<any>;
items: QueryList<DropdownItemComponent>;
readonly closed: EventEmitter<void>;
readonly yPosition: 'top' | 'bottom';
readonly xPosition: 'start' | 'end';
readonly id: string;
}
Original file line number Diff line number Diff line change
@@ -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<HTMLElement>, 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<MouseEvent | void> {
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();
}
}
}
Empty file.
14 changes: 14 additions & 0 deletions projects/ng-tw/src/modules/dropdown/dropdown.component.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<ng-template>
<div
(click)="closed.emit()"
class="w-56 rounded-md shadow-lg bg-white ring-1 ring-black ring-opacity-5 focus:outline-none"
role="menu"
aria-orientation="vertical"
aria-labelledby="menu-button"
tabindex="-1"
>
<div class="py-1">
<ng-content></ng-content>
</div>
</div>
</ng-template>
25 changes: 25 additions & 0 deletions projects/ng-tw/src/modules/dropdown/dropdown.component.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';

import { DropdownComponent } from './dropdown.component';

describe('DropdownComponent', () => {
let component: DropdownComponent;
let fixture: ComponentFixture<DropdownComponent>;

beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ DropdownComponent ]
})
.compileComponents();
});

beforeEach(() => {
fixture = TestBed.createComponent(DropdownComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});

it('should create', () => {
expect(component).toBeTruthy();
});
});
40 changes: 40 additions & 0 deletions projects/ng-tw/src/modules/dropdown/dropdown.component.ts
Original file line number Diff line number Diff line change
@@ -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<any>;
@ContentChildren(DropdownItemComponent, { descendants: true }) public items!: QueryList<DropdownItemComponent>;

@Output() public closed = new EventEmitter<void>();

@Input() public yPosition: 'top' | 'bottom' = 'bottom';
@Input() public xPosition: 'start' | 'end' = 'start';

@Input() public id: string = `tw-dropdown-${_uniqueIdCounter++}`;
}
13 changes: 13 additions & 0 deletions projects/ng-tw/src/modules/dropdown/dropdown.module.ts
Original file line number Diff line number Diff line change
@@ -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 {}
6 changes: 6 additions & 0 deletions projects/ng-tw/src/public-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,20 @@ <h1 id="introduction">
<b>ng-tw</b> is a package to help you build a complete web application with Angular and Tailwind CSS. It's <b>an implementation of tailwind</b> components that requires javascript to run, <b>the logic</b> of these components <b>runs on angular</b>.
</p>

<button
[twDropdownTriggerFor]="dropdown"
class="button"
>
Open dropdown
</button>

<tw-dropdown #dropdown>
<div tw-dropdown-item (click)="teste()">teste</div>
<div tw-dropdown-item>teste 2</div>
<div tw-dropdown-item>teste 4</div>
</tw-dropdown>


<h2 id="components">
Components
</h2>
Expand All @@ -15,6 +29,7 @@ <h2 id="components">
<div
class="mt-3 flex"
*ngFor="let item of components"
[routerLink]="[item.link]"
>
<span class="mt-px mr-3 flex-shrink-0">
<svg
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,4 +36,8 @@ export class IntroductionRouteComponent implements OnInit {
constructor() {}

ngOnInit(): void {}

teste() {
console.log('teste');
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { CommonModule } from '@angular/common';
import { IntroductionRouteComponent } from './introduction-route.component';
import { Routes, RouterModule } from '@angular/router';
import { IntroductionSideRouteComponent } from '../introduction-side-route/introduction-side-route.component';
import { TwButtonModule, TwDropdownModule } from 'ng-tw';

const routes: Routes = [
{
Expand All @@ -18,6 +19,6 @@ const routes: Routes = [

@NgModule({
declarations: [IntroductionRouteComponent, IntroductionSideRouteComponent],
imports: [CommonModule, RouterModule.forChild(routes)],
imports: [CommonModule, RouterModule.forChild(routes), TwDropdownModule],
})
export class IntroductionRouteModule {}

0 comments on commit 7857b1c

Please sign in to comment.