Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(ui-kit): ported toggletip module from Hanami #83

Merged
merged 1 commit into from
Feb 16, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion libs/ui-kit/ng-package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,6 @@
"dest": "../../dist/libs/ui-kit",
"lib": {
"entryFile": "src/index.ts"
}
},
"assets": ["styles/**/*.scss"]
}
3 changes: 3 additions & 0 deletions libs/ui-kit/styles/main.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
html {
font-size: var(--cz-font-size);
}
6 changes: 6 additions & 0 deletions libs/ui-kit/styles/themes/default.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
:root {
--cz-color-white: #ffffff;
--cz-font-size: 1rem;
--cz-font-color-high: #1f1f1f;
--cz-font-color-medium: #505050;
}
3 changes: 3 additions & 0 deletions libs/ui-kit/toggletip/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export * from './toggletip-trigger-for.directive';
export * from './toggletip.component';
export * from './toggletip.module';
5 changes: 5 additions & 0 deletions libs/ui-kit/toggletip/ng-package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"lib": {
"entryFile": "index.ts"
}
}
132 changes: 132 additions & 0 deletions libs/ui-kit/toggletip/toggletip-trigger-for.directive.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
import { Overlay, OverlayRef } from '@angular/cdk/overlay';
import { TemplatePortal } from '@angular/cdk/portal';
import {
Directive,
ElementRef,
HostBinding,
HostListener,
inject,
Injector,
Input,
NgZone,
OnDestroy,
ViewContainerRef,
} from '@angular/core';
import { OnDestroy$ } from '@cognizone/ng-core';
import { filter, first, fromEvent } from 'rxjs';

import { ToggletipComponent } from './toggletip.component';

@Directive({
selector: '[czToggletipTriggerFor]',
exportAs: 'czToggletipTriggerFor',
standalone: true,
})
export class ToggletipTriggerForDirective extends OnDestroy$ implements OnDestroy {
@Input('czToggletipTriggerFor')
toggletip?: ToggletipComponent;

@HostBinding('attr.aria-expanded')
opened = false;

@HostBinding('attr.aria-haspopup')
hasPopup = true;

@HostBinding('attr.aria-controls')
get ariaControls(): string | undefined {
return this.opened ? this.toggletip?.dialogId : undefined;
}

private injector = inject(Injector);
private viewContainerRef = inject(ViewContainerRef);
private overlay = inject(Overlay);
private elRef = inject(ElementRef);
private ngZone = inject(NgZone);
private overlayRef?: OverlayRef;
private portal?: TemplatePortal;

private processingClick = false;

@HostListener('click')
async onClick(): Promise<void> {
if (this.processingClick) return;
this.processingClick = true;
if (!this.toggletip) return;

const boundingBox = this.elRef.nativeElement.getBoundingClientRect();
const positionClass = boundingBox.left < window.innerWidth / 2 ? 'is-left' : 'is-right';

if (!this.overlayRef) {
const positionStrategy = this.overlay
.position()
.flexibleConnectedTo(this.elRef)
.withPositions([
{
originX: positionClass === 'is-left' ? 'end' : 'start',
originY: 'center',
overlayX: positionClass === 'is-left' ? 'start' : 'end',
overlayY: 'center',
},
])
.withDefaultOffsetX(positionClass === 'is-left' ? 8 : -8);

this.overlayRef = this.overlay.create({
positionStrategy,
scrollStrategy: this.overlay.scrollStrategies.reposition(),
panelClass: positionClass,
});
} else {
this.overlayRef.detach();
await new Promise(resolve => setTimeout(resolve, 50));
}

this.portal = new TemplatePortal(this.toggletip.tpl, this.viewContainerRef, undefined, this.injector);
this.subSink = this.toggletip.toggletipClose.subscribe(() => this.close());
this.overlayRef.attach(this.portal);
this.initInteractions();
this.subSink = this.ngZone.onStable.pipe(first()).subscribe(() => {
this.overlayRef?.overlayElement.querySelector('button')?.focus();
});
this.opened = true;
this.processingClick = false;
}

@HostListener('window:keydown.escape')
close(): void {
if (!this.overlayRef?.hasAttached()) return;
this.emptySink();
const hasFocus = !!this.overlayRef.overlayElement.querySelector('*:focus-within');
if (hasFocus) {
this.elRef.nativeElement.focus();
}
this.overlayRef.detach();
this.opened = false;
}

override ngOnDestroy(): void {
super.ngOnDestroy();
this.close();
}

private initInteractions(): void {
const overlayEl = this.overlayRef?.overlayElement;
if (!overlayEl) return;

this.subSink = fromEvent<KeyboardEvent>(overlayEl, 'keydown')
.pipe(filter(e => e.key === 'Tab'))
.subscribe(event => {
const list = this.getAllFocusableElements(overlayEl);
const target = event.getModifierState('Shift') ? list.item(0) : list.item(list.length - 1);
if (event.target === target) {
event.preventDefault();
this.close();
}
});
}

getAllFocusableElements<T extends Element = Element>(parent: HTMLElement): NodeListOf<T> {
return parent.querySelectorAll(
'button:not([disabled]), [href], input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"]):not([disabled]), details:not([disabled]), summary:not(:disabled)'
);
}
}
59 changes: 59 additions & 0 deletions libs/ui-kit/toggletip/toggletip.component.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
cz-toggletip {
display: none;
}

.cz-toggletip-content {
--bg-color: var(--cz-color-white);
--font-size: 0.875rem;
position: relative;
max-width: 23rem;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

oui :D can't we have this in px? (since we want to use px for width at this moment 🤔 )

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here it's one of the cases where width in rem makes more sense -> if the user sets a high font size, it's better to leave it some more breathing room, otherwise with a width in px it will feel cramped 😅

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ok, good to know :)

padding: 1.5rem;
background-color: var(--bg-color);
box-shadow: 0 2px 8px 0 rgba(40, 41, 61, 0.04), 0 16px 24px 0 rgba(96, 97, 112, 0.16);
font-size: var(--font-size);

.cz-toggletip-close {
position: absolute;
top: 4px;
right: 4px;

height: 24px;
width: 24px;
border-radius: 4px;
.mat-button-wrapper {
display: flex;
align-items: center;
justify-content: center;
}
mat-icon.mat-icon {
font-size: 16px;
width: 16px;
height: 16px;
line-height: 16px;
color: var(--cz-font-color-high);
}
}

&::after {
top: 50%;
transform: translate(0, -50%);
position: absolute;
content: '';
width: 0;
height: 0;
border-top: 5px solid transparent;
border-bottom: 5px solid transparent;
}
}

.is-left .cz-toggletip-content::after {
right: 100%;
border-right: 7px solid var(--bg-color);
border-left: 5px solid transparent;
}

.is-right .cz-toggletip-content::after {
left: 100%;
border-left: 7px solid var(--bg-color);
border-right: 5px solid transparent;
}
34 changes: 34 additions & 0 deletions libs/ui-kit/toggletip/toggletip.component.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { ChangeDetectionStrategy, Component, EventEmitter, Output, TemplateRef, ViewChild, ViewEncapsulation } from '@angular/core';
import { MatIconModule } from '@angular/material/icon';

import { MatButtonModule } from '@angular/material/button';

let instanceId = 0;

@Component({
selector: 'cz-toggletip',
template: `
<ng-template #tpl>
<div [id]="dialogId" class="cz-toggletip-content" role="dialog" tabindex="-1">
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the [id] can come after class/role etc :)

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

<button class="cz-toggletip-close" mat-icon-button (click)="toggletipClose.emit()">
<mat-icon>close</mat-icon>
</button>
<ng-content></ng-content>
</div>
</ng-template>
`,
styleUrls: ['./toggletip.component.scss'],
encapsulation: ViewEncapsulation.None,
changeDetection: ChangeDetectionStrategy.OnPush,
standalone: true,
imports: [MatButtonModule, MatIconModule],
})
export class ToggletipComponent {
@ViewChild('tpl', { static: true })
tpl!: TemplateRef<unknown>;

dialogId = `cz-toggletip-${instanceId++}`;

@Output()
toggletipClose = new EventEmitter<void>();
}
10 changes: 10 additions & 0 deletions libs/ui-kit/toggletip/toggletip.module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { NgModule } from '@angular/core';

import { ToggletipTriggerForDirective } from './toggletip-trigger-for.directive';
import { ToggletipComponent } from './toggletip.component';

@NgModule({
imports: [ToggletipTriggerForDirective, ToggletipComponent],
exports: [ToggletipTriggerForDirective, ToggletipComponent],
})
export class ToggletipModule {}
Loading