diff --git a/README.md b/README.md
index ae34f42..2d97b98 100644
--- a/README.md
+++ b/README.md
@@ -4,6 +4,19 @@ ngSketch is an open-source headless library for Angular 18+. It peppers your app
Get inspired by opinionated demos and use the theme that fits your application.
+## Use ngSketch
+
+In order for all features to work, you need to add the `SketchModule` to the `appConfig` of you application.
+
+```ts
+export const appConfig: ApplicationConfig = {
+ providers: [
+ // ...
+ importProvidersFrom(SketchModule),
+ ],
+};
+```
+
## Storybook
Deployed from main: https://661d194a3f6d14d86c328554-yfvvdffiwi.chromatic.com/
diff --git a/apps/demo-app/src/app/app.config.ts b/apps/demo-app/src/app/app.config.ts
index f95c7ed..fc7be79 100644
--- a/apps/demo-app/src/app/app.config.ts
+++ b/apps/demo-app/src/app/app.config.ts
@@ -7,6 +7,7 @@ import {
} from '@angular/router';
import { provideAnimations } from '@angular/platform-browser/animations';
import { FontAwesomeModule } from '@fortawesome/angular-fontawesome';
+import { SketchModule } from '@qupaya/sketch';
export const appConfig: ApplicationConfig = {
providers: [
@@ -17,5 +18,6 @@ export const appConfig: ApplicationConfig = {
),
provideAnimations(),
importProvidersFrom(FontAwesomeModule),
+ importProvidersFrom(SketchModule),
],
};
diff --git a/apps/demo-app/src/app/pages/dialog-sample/dialog-sample.component.html b/apps/demo-app/src/app/pages/dialog-sample/dialog-sample.component.html
index 3341e1f..7604518 100644
--- a/apps/demo-app/src/app/pages/dialog-sample/dialog-sample.component.html
+++ b/apps/demo-app/src/app/pages/dialog-sample/dialog-sample.component.html
@@ -429,4 +429,30 @@
+
+
+
+
+
+
+ Here is some Content.
+
+
+ Placeholder
+ Selected Item
+
+ Option 1
+ Option 2
+ Option 3
+ Option 4
+ Option 5
+
+
+
+
diff --git a/apps/demo-app/src/app/pages/dialog-sample/dialog-sample.component.ts b/apps/demo-app/src/app/pages/dialog-sample/dialog-sample.component.ts
index 8b51fc6..7620038 100644
--- a/apps/demo-app/src/app/pages/dialog-sample/dialog-sample.component.ts
+++ b/apps/demo-app/src/app/pages/dialog-sample/dialog-sample.component.ts
@@ -1,11 +1,15 @@
import { Component } from '@angular/core';
-import { CommonModule } from '@angular/common';
-import { CloseButtonPosition, DialogComponent } from '@qupaya/sketch';
+import {
+ CloseButtonPosition,
+ DialogComponent,
+ SelectComponent,
+ SelectOptionComponent,
+} from '@qupaya/sketch';
@Component({
selector: 'app-dialog-sample',
standalone: true,
- imports: [CommonModule, DialogComponent],
+ imports: [DialogComponent, SelectComponent, SelectOptionComponent],
templateUrl: './dialog-sample.component.html',
styleUrl: './dialog-sample.component.css',
})
@@ -24,6 +28,7 @@ export class DialogSampleComponent {
isDialogWithNestedDialogOpen = false;
isNestedDialogOpen = false;
isDefaultContentShadowOpen = false;
+ isDialogWithSelectOpen = false;
readonly CloseButtonPosition = CloseButtonPosition;
}
diff --git a/libs/sketch/src/index.ts b/libs/sketch/src/index.ts
index eff0b5c..d38e028 100644
--- a/libs/sketch/src/index.ts
+++ b/libs/sketch/src/index.ts
@@ -2,3 +2,5 @@ export * from './lib/components/sketch/sketch.component';
export * from './lib/components/select';
export * from './lib/components/list';
export * from './lib/components/dialog/dialog.component';
+export * from './lib/components/overlay/overlay.directive';
+export * from './lib/sketch.module';
diff --git a/libs/sketch/src/lib/components/dialog/dialog.component.html b/libs/sketch/src/lib/components/dialog/dialog.component.html
index 5a6a1e6..2ecd968 100644
--- a/libs/sketch/src/lib/components/dialog/dialog.component.html
+++ b/libs/sketch/src/lib/components/dialog/dialog.component.html
@@ -34,4 +34,5 @@
+
diff --git a/libs/sketch/src/lib/components/dialog/dialog.component.stories.ts b/libs/sketch/src/lib/components/dialog/dialog.component.stories.ts
index 3dcb058..c989711 100644
--- a/libs/sketch/src/lib/components/dialog/dialog.component.stories.ts
+++ b/libs/sketch/src/lib/components/dialog/dialog.component.stories.ts
@@ -1,5 +1,4 @@
import { ArgTypes, Meta, moduleMetadata, StoryObj } from '@storybook/angular';
-import { CommonModule } from '@angular/common';
import { CloseButtonPosition, DialogComponent } from './dialog.component';
const meta: Meta = {
@@ -7,11 +6,7 @@ const meta: Meta = {
tags: ['autodocs'],
parameters: {},
title: 'DialogComponent',
- decorators: [
- moduleMetadata({
- imports: [CommonModule],
- }),
- ],
+ decorators: [moduleMetadata({})],
};
export default meta;
type Story = StoryObj;
diff --git a/libs/sketch/src/lib/components/dialog/dialog.component.ts b/libs/sketch/src/lib/components/dialog/dialog.component.ts
index cfb7819..c676ce0 100644
--- a/libs/sketch/src/lib/components/dialog/dialog.component.ts
+++ b/libs/sketch/src/lib/components/dialog/dialog.component.ts
@@ -1,15 +1,17 @@
import {
Component,
- effect,
input,
output,
viewChild,
model,
- untracked,
ElementRef,
+ effect,
+ untracked,
+ inject,
} from '@angular/core';
import { ClickBackdropDirective } from './directive/click-backdrop.directive';
import { NgClass } from '@angular/common';
+import { SkOverlayContainer } from '../overlay/overlay-container';
export enum CloseButtonPosition {
Left = 'left',
@@ -24,6 +26,11 @@ export enum CloseButtonPosition {
styleUrl: './dialog.component.css',
})
export class DialogComponent {
+ private readonly overlayContainer = inject(SkOverlayContainer);
+ private readonly dialogOverlayContainerRef = viewChild.required<
+ ElementRef
+ >('dialogOverlayContainer');
+
readonly open = model(false);
readonly showCloseButton = input(false);
@@ -48,11 +55,14 @@ export class DialogComponent {
protected readonly openEvents = effect(
() => {
const dialog = untracked(this.dialogElement);
+ const containerRef = untracked(this.dialogOverlayContainerRef);
if (this.open()) {
dialog.nativeElement.showModal();
+ this.overlayContainer.addContainer(containerRef.nativeElement);
} else {
dialog.nativeElement.close();
+ this.overlayContainer.removeContainer();
}
},
{ allowSignalWrites: true }
diff --git a/libs/sketch/src/lib/components/overlay/overlay-container.ts b/libs/sketch/src/lib/components/overlay/overlay-container.ts
new file mode 100644
index 0000000..2748ce4
--- /dev/null
+++ b/libs/sketch/src/lib/components/overlay/overlay-container.ts
@@ -0,0 +1,20 @@
+import { Injectable } from '@angular/core';
+import { OverlayContainer } from '@angular/cdk/overlay';
+
+@Injectable({
+ providedIn: 'root',
+})
+export class SkOverlayContainer extends OverlayContainer {
+ private readonly containerStack: HTMLElement[] = [];
+
+ addContainer(container: HTMLElement): void {
+ if (this._containerElement) {
+ this.containerStack.push(this._containerElement);
+ }
+ this._containerElement = container;
+ }
+
+ removeContainer(): void {
+ this._containerElement = this.containerStack.pop() as HTMLElement;
+ }
+}
diff --git a/libs/sketch/src/lib/components/select/directives/overlay.directive.ts b/libs/sketch/src/lib/components/overlay/overlay.directive.ts
similarity index 61%
rename from libs/sketch/src/lib/components/select/directives/overlay.directive.ts
rename to libs/sketch/src/lib/components/overlay/overlay.directive.ts
index 8db97e3..36fee86 100644
--- a/libs/sketch/src/lib/components/select/directives/overlay.directive.ts
+++ b/libs/sketch/src/lib/components/overlay/overlay.directive.ts
@@ -1,4 +1,8 @@
+import { ConnectedPosition, Overlay, OverlayRef } from '@angular/cdk/overlay';
+import { CdkPortal } from '@angular/cdk/portal';
+import { DOCUMENT } from '@angular/common';
import {
+ DestroyRef,
Directive,
effect,
ElementRef,
@@ -6,11 +10,8 @@ import {
input,
output,
} from '@angular/core';
-import { CdkPortal } from '@angular/cdk/portal';
-import { ConnectedPosition, Overlay, OverlayRef } from '@angular/cdk/overlay';
-import { DOCUMENT } from '@angular/common';
+import { takeUntilDestroyed, toSignal } from '@angular/core/rxjs-interop';
import { debounceTime, fromEvent, merge } from 'rxjs';
-import { toSignal } from '@angular/core/rxjs-interop';
export const DEFAULT_POSITIONS: ConnectedPosition[] = [
{ originX: 'end', originY: 'bottom', overlayX: 'end', overlayY: 'top' },
@@ -23,48 +24,54 @@ export const DEFAULT_POSITIONS: ConnectedPosition[] = [
export class CdkOverlayDirective {
private readonly overlay = inject(Overlay);
private readonly elementRef = inject(ElementRef);
+ private readonly destroyRef = inject(DestroyRef);
private readonly window = inject(DOCUMENT)?.defaultView;
private readonly windowResize = this.window
? toSignal(fromEvent(this.window, 'resize').pipe(debounceTime(500)))
: undefined;
- portal = input(undefined, { alias: 'skCdkOverlay' });
- showOverlay = input(false, { alias: 'skCdkOverlayShow' });
- connectedPositions = input(DEFAULT_POSITIONS, {
+ readonly portal = input(undefined, {
+ alias: 'skCdkOverlay',
+ });
+ readonly showOverlay = input(false, { alias: 'skCdkOverlayShow' });
+ readonly connectedPositions = input(DEFAULT_POSITIONS, {
alias: 'skCdkOverlayPositions',
});
- disposeDelay = input(0, { alias: 'skCdkOverlayDisposeDelay' });
- relativeTo = input(undefined, {
+ readonly disposeDelay = input(0, { alias: 'skCdkOverlayDisposeDelay' });
+ readonly relativeTo = input(undefined, {
alias: 'skCdkOverlayRelativeTo',
});
- backdropClass = input('cdk-overlay-transparent-backdrop', {
+ readonly backdropClass = input('cdk-overlay-transparent-backdrop', {
alias: 'skCdkOverlayBackdropClass',
});
- panelClass = input('cdk-overlay-panel', {
+ readonly panelClass = input('cdk-overlay-panel', {
alias: 'skCdkOverlayPanelClass',
});
- offsetX = input(0, {
+ readonly offsetX = input(0, {
alias: 'skCdkOverlayOffsetX',
});
- offsetY = input(0, {
+ readonly offsetY = input(0, {
alias: 'skCdkOverlayOffsetY',
});
- visible = output({ alias: 'skCdkOverlayVisible' });
+ readonly visible = output({ alias: 'skCdkOverlayVisible' });
private _overlayRef?: OverlayRef;
private _relatedElement?: HTMLElement =
this.relativeTo() || this.elementRef.nativeElement;
- protected readonly detectVisibleChange = effect(() => {
- if (this._relatedElement) {
- if (this.showOverlay()) {
- this.createOverlay();
- } else {
- this.hide();
+ protected readonly detectVisibleChange = effect(
+ () => {
+ if (this._relatedElement) {
+ if (this.showOverlay()) {
+ this.createOverlay();
+ } else {
+ this.hide();
+ }
}
- }
- });
+ },
+ { allowSignalWrites: true }
+ );
protected readonly updateOverlayPortal = effect(() => {
if (this.windowResize && this.windowResize()) {
@@ -77,6 +84,10 @@ export class CdkOverlayDirective {
return;
}
+ if (this._overlayRef?.hasAttached()) {
+ return;
+ }
+
const positionStrategy = this.overlay
.position()
.flexibleConnectedTo(this._relatedElement)
@@ -86,20 +97,28 @@ export class CdkOverlayDirective {
.withDefaultOffsetY(this.offsetY())
.withFlexibleDimensions(false);
+ const scrollStrategy = this.overlay.scrollStrategies.reposition();
+
this._overlayRef = this.overlay.create({
hasBackdrop: true,
backdropClass: this.backdropClass(),
panelClass: this.panelClass(),
positionStrategy,
+ scrollStrategy,
});
this.syncOverlayWidth();
+ this._overlayRef
+ .attachments()
+ .pipe(takeUntilDestroyed(this.destroyRef))
+ .subscribe(() => this.visible.emit(true));
+
+ merge(this._overlayRef.backdropClick(), this._overlayRef.detachments())
+ .pipe(takeUntilDestroyed(this.destroyRef))
+ .subscribe(() => this.hide());
+
this._overlayRef.attach(this.portal());
- merge(
- this._overlayRef.backdropClick(),
- this._overlayRef.detachments()
- ).subscribe(() => this.hide());
}
private hide(): void {
diff --git a/libs/sketch/src/lib/components/select/index.ts b/libs/sketch/src/lib/components/select/index.ts
index ad1b0c7..6b06198 100644
--- a/libs/sketch/src/lib/components/select/index.ts
+++ b/libs/sketch/src/lib/components/select/index.ts
@@ -1,3 +1,2 @@
export * from './components/select-option/select-option.component';
export * from './select.component';
-export * from './directives/overlay.directive';
diff --git a/libs/sketch/src/lib/components/select/select.component.ts b/libs/sketch/src/lib/components/select/select.component.ts
index 9d2308f..78045c9 100644
--- a/libs/sketch/src/lib/components/select/select.component.ts
+++ b/libs/sketch/src/lib/components/select/select.component.ts
@@ -13,16 +13,15 @@ import {
untracked,
ViewEncapsulation,
} from '@angular/core';
-import { CommonModule } from '@angular/common';
-import { CdkOverlayDirective } from './directives/overlay.directive';
import { CdkPortal } from '@angular/cdk/portal';
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
import { CdkTrapFocus } from '@angular/cdk/a11y';
+import { CdkOverlayDirective } from '../overlay/overlay.directive';
@Component({
selector: 'sk-select',
standalone: true,
- imports: [CommonModule, CdkOverlayDirective, CdkPortal, CdkTrapFocus],
+ imports: [CdkOverlayDirective, CdkPortal, CdkTrapFocus],
providers: [
{
provide: NG_VALUE_ACCESSOR,
diff --git a/libs/sketch/src/lib/sketch.module.ts b/libs/sketch/src/lib/sketch.module.ts
new file mode 100644
index 0000000..51886f7
--- /dev/null
+++ b/libs/sketch/src/lib/sketch.module.ts
@@ -0,0 +1,13 @@
+import { NgModule } from '@angular/core';
+import { OverlayContainer } from '@angular/cdk/overlay';
+import { SkOverlayContainer } from './components/overlay/overlay-container';
+
+@NgModule({
+ providers: [
+ {
+ provide: OverlayContainer,
+ useExisting: SkOverlayContainer,
+ },
+ ],
+})
+export class SketchModule {}