Skip to content

Commit 484a980

Browse files
committed
reworked menus
1 parent 83d8cd2 commit 484a980

32 files changed

+614
-478
lines changed

projects/component-library/dialog/src/dialog.args.ts

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -19,12 +19,14 @@ export interface IDialogHeaderArgs {
1919
}
2020

2121
export interface IDialogSizeArgs {
22-
width?: string | ((referenceElementRect: DOMRect) => string);
23-
height?: string | ((referenceElementRect: DOMRect) => string);
24-
maxWidth?: string;
25-
maxHeight?: string;
22+
width?: string | ((referenceElementRect: DOMRect) => string | undefined);
23+
height?: string | ((referenceElementRect: DOMRect) => string | undefined);
24+
minWidth?: string | ((referenceElementRect: DOMRect) => string | undefined);
25+
minHeight?: string | ((referenceElementRect: DOMRect) => string | undefined);
26+
maxWidth?: string | ((referenceElementRect: DOMRect) => string | undefined);
27+
maxHeight?: string | ((referenceElementRect: DOMRect) => string | undefined);
2628
}
2729
export interface IDialogPositionAndSizeArgs extends IDialogSizeArgs {
28-
top?: string | ((actualWidth: number, actualHeight: number, referenceElementRect?: DOMRect) => string);
29-
left?: string | ((actualWidth: number, actualHeight: number, referenceElementRect?: DOMRect) => string);
30+
top?: string | ((actualWidth: number, actualHeight: number, referenceElementRect?: DOMRect) => string | undefined);
31+
left?: string | ((actualWidth: number, actualHeight: number, referenceElementRect?: DOMRect) => string | undefined);
3032
}

projects/component-library/dialog/src/dialog.service.ts

Lines changed: 74 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ import type { FocusableElement } from 'tabbable';
88
import { tabbable } from 'tabbable';
99
import type { IConfirmationDialogData } from './confirmation-dialog.component';
1010
import { ConfirmationDialogComponent } from './confirmation-dialog.component';
11-
import { TemplateDialogComponent } from './template-dialog.component';
1211
import { TAB_DATA_REF } from './data.ref';
1312
import { IconComponent } from 'tableau-ui-angular/icon';
1413
import type { StackOptions } from './stack-options';
@@ -24,6 +23,22 @@ export class DialogService {
2423
environmentInjector = inject(EnvironmentInjector);
2524

2625
openModal<TComponent, TData, TResult>(component: Type<TComponent>, data: TData, args?: IModalArgs): DialogRef<TResult> {
26+
return this._openModal<TData, TResult>(
27+
(injector: Injector) => this.createView(component, injector),
28+
data,
29+
args,
30+
);
31+
}
32+
33+
openTemplateModal<TContext, TResult>(contentTemplate: TemplateRef<TContext>, contentTemplateContext: TContext, args?: IModalArgs): DialogRef<TResult> {
34+
return this._openModal<TContext, TResult>(
35+
(injector: Injector) => contentTemplate.createEmbeddedView(contentTemplateContext, injector),
36+
contentTemplateContext,
37+
args,
38+
);
39+
}
40+
41+
_openModal<TData, TResult>(getViewRef: (injector: Injector) => ViewRef, data: TData, args?: IModalArgs): DialogRef<TResult> {
2742
const a = {
2843
width: args?.width ?? '300px',
2944
height: args?.height ?? 'fit-content',
@@ -56,14 +71,10 @@ export class DialogService {
5671
trapFocus: true,
5772
} as IDialogArgs;
5873

59-
const ref = this.openDialog<TComponent, TData, TResult>(component, data, a);
74+
const ref = this._openDialog<TData, TResult>(getViewRef, data, a);
6075
return ref;
6176
}
6277

63-
openTemplateModal<TContext, TResult>(contentTemplate: TemplateRef<TContext>, contentTemplateContext?: TContext, args?: IModalArgs): DialogRef<TResult> {
64-
return this.openModal(TemplateDialogComponent, { contentTemplate, contentTemplateContext }, args);
65-
}
66-
6778
async openConfirmationMessageDialog(
6879
title: string,
6980
message: string,
@@ -136,14 +147,25 @@ export class DialogService {
136147
args: IDialogArgs;
137148
}[] = [];
138149

139-
openTemplateDialog<TContext, TResult>(contentTemplate: TemplateRef<TContext>, args: IDialogArgs, contentTemplateContext?: TContext, stackOptions: StackOptions = new GlobalStackOptions()) {
140-
return this.openDialog<
141-
TemplateDialogComponent<{ contentTemplate: TemplateRef<TContext | undefined>; contentTemplateContext: TContext | undefined }>,
142-
{ contentTemplate: TemplateRef<TContext>; contentTemplateContext?: TContext },
143-
TResult
144-
>(TemplateDialogComponent, { contentTemplate, contentTemplateContext }, args, stackOptions);
150+
openTemplateDialog<TContext, TResult>(contentTemplate: TemplateRef<TContext>, args: IDialogArgs, contentTemplateContext: TContext, stackOptions: StackOptions = new GlobalStackOptions()) {
151+
152+
return this._openDialog<TContext, TResult>(
153+
(injector: Injector) => contentTemplate.createEmbeddedView(contentTemplateContext, injector),
154+
contentTemplateContext,
155+
args,
156+
stackOptions
157+
);
145158
}
159+
146160
openDialog<TComponent, TData, TResult>(component: Type<TComponent>, data: TData, args: IDialogArgs = {}, stackOptions: StackOptions = new GlobalStackOptions()): DialogRef<TResult> {
161+
return this._openDialog<TData, TResult>(
162+
(injector: Injector) => this.createView(component, injector),
163+
data,
164+
args,
165+
stackOptions
166+
);
167+
}
168+
private _openDialog<TData, TResult>(getViewRef: (injector: Injector) => ViewRef, data: TData, args: IDialogArgs = {}, stackOptions: StackOptions = new GlobalStackOptions()): DialogRef<TResult> {
147169
let trappedFocus:
148170
| {
149171
elements: {
@@ -181,7 +203,7 @@ export class DialogService {
181203
});
182204

183205
// Create the component view
184-
const componentView = this.createView(component, injector);
206+
const componentView = getViewRef(injector);
185207
// Attach component to the application
186208
this.appRef.attachView(componentView);
187209

@@ -290,9 +312,15 @@ export class DialogService {
290312
}
291313

292314
private createDialogElement(viewRef: ViewRef, args: IDialogArgs, injector: Injector, dialogRef: IDialogRef, zIndex: number): HTMLElement {
293-
const dialogElement = (viewRef as EmbeddedViewRef<unknown>).rootNodes[0] as HTMLElement;
315+
const embeddedViewRef = viewRef as EmbeddedViewRef<unknown>;
316+
317+
const dialogElement = document.createElement('div');
294318
dialogElement.classList.add('dialog-container');
295319
dialogElement.style.zIndex = zIndex.toString();
320+
for (const rootNode of embeddedViewRef.rootNodes) {
321+
dialogElement.appendChild(rootNode as Node);
322+
}
323+
296324

297325
if (args.containerCss) {
298326
Object.keys(args.containerCss).forEach((key) => {
@@ -380,55 +408,44 @@ export class DialogService {
380408
return undefined;
381409
}
382410
private static calculateAndSetPosition(dialogElement: HTMLElement, args: IDialogPositionAndSizeArgs, stackOptions: StackOptions) {
383-
if (args.maxWidth !== undefined) {
384-
dialogElement.style.maxWidth = args.maxWidth;
385-
}
386-
if (args.maxHeight !== undefined) {
387-
dialogElement.style.maxHeight = args.maxHeight;
388-
}
389-
dialogElement.style.overflowX = 'hidden';
390-
dialogElement.style.overflowY = 'hidden';
391-
392411
const referenceElement = this.getReferenceElement(stackOptions);
393412
const referenceElementRect = referenceElement?.getBoundingClientRect();
394-
let widthCss: string;
395-
let heightCss: string;
396-
if (args.width === undefined) {
397-
widthCss = 'fit-content';
398-
} else if (typeof args.width === 'function') {
399-
if (!referenceElementRect) {
400-
throw new Error('When using a function for width, insertAfterElement must be provided.');
413+
const getWidthHeight = (name: string, defaultValue: string, value: string | ((referenceElementRect: DOMRect) => string | undefined) | undefined) => {
414+
if (value === undefined) {
415+
return defaultValue;
416+
} else if (typeof value === 'function') {
417+
if (!referenceElementRect) {
418+
throw new Error(`When using a function for ${name}, insertAfterElement must be provided.`);
419+
}
420+
return value(referenceElementRect) ?? defaultValue;
421+
} else {
422+
return value;
401423
}
402-
widthCss = args.width(referenceElementRect);
403-
} else {
404-
widthCss = args.width;
405-
}
406-
if (args.height === undefined) {
407-
heightCss = 'fit-content';
408-
} else if (typeof args.height === 'function') {
409-
if (!referenceElementRect) {
410-
throw new Error('When using a function for height, insertAfterElement must be provided.');
411-
}
412-
heightCss = args.height(referenceElementRect);
413-
} else {
414-
heightCss = args.height;
415-
}
416-
dialogElement.style.height = heightCss;
417-
dialogElement.style.width = widthCss;
424+
};
425+
426+
dialogElement.style.minWidth = getWidthHeight('minWidth', 'auto', args.minWidth);
427+
dialogElement.style.minHeight = getWidthHeight('minHeight', '0', args.minHeight);
428+
dialogElement.style.width = getWidthHeight('width', 'fit-content', args.width);
429+
dialogElement.style.height = getWidthHeight('height', 'fit-content', args.height);
430+
dialogElement.style.maxWidth = getWidthHeight('maxWidth', 'none', args.maxWidth);
431+
dialogElement.style.maxHeight = getWidthHeight('maxHeight', 'none', args.maxHeight);
432+
433+
dialogElement.style.overflowX = 'hidden';
434+
dialogElement.style.overflowY = 'hidden';
418435
const actualWidth = dialogElement.offsetWidth;
419436
const actualHeight = dialogElement.offsetHeight;
420437

421-
if (typeof args.top === 'function') {
422-
dialogElement.style.top = args.top(actualWidth, actualHeight, referenceElementRect);
423-
} else if (typeof args.top === 'string') {
424-
dialogElement.style.top = args.top;
425-
}
426-
427-
if (typeof args.left === 'function') {
428-
dialogElement.style.left = args.left(actualWidth, actualHeight, referenceElementRect);
429-
} else if (typeof args.left === 'string') {
430-
dialogElement.style.left = args.left;
431-
}
438+
const getTopLeft = (name: string, value: string | ((actualWidth: number, actualHeight: number, referenceElementRect?: DOMRect) => string | undefined) | undefined) => {
439+
if (value === undefined) {
440+
return 'auto';
441+
} else if (typeof value === 'function') {
442+
return value(actualWidth, actualHeight, referenceElementRect) ?? 'auto';
443+
} else {
444+
return value;
445+
}
446+
};
447+
dialogElement.style.top = getTopLeft('top', args.top);
448+
dialogElement.style.left = getTopLeft('left', args.left);
432449

433450
// if dialog is higher than the available page height, set it to scroll
434451
if (actualHeight + dialogElement.offsetTop > window.innerHeight) {
Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,11 @@
11
import { CommonModule } from '@angular/common';
22
import { NgModule } from '@angular/core';
33
import { ConfirmationDialogComponent } from './confirmation-dialog.component';
4-
import { TemplateDialogComponent } from './template-dialog.component';
54
import { TableauUiButtonModule } from 'tableau-ui-angular/button';
65
import { DialogService } from './dialog.service';
76
@NgModule({
87
imports: [CommonModule, TableauUiButtonModule],
9-
declarations: [ConfirmationDialogComponent, TemplateDialogComponent],
8+
declarations: [ConfirmationDialogComponent],
109
providers: [DialogService],
1110
})
1211
export class TableauUiDialogModule {}

projects/component-library/dialog/src/template-dialog.component.ts

Lines changed: 0 additions & 18 deletions
This file was deleted.
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
{}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import { ButtonMenuDirective } from './button-menu.directive';
2+
3+
describe('ButtonMenuDirective', () => {
4+
it('should create an instance', () => {
5+
const directive = new ButtonMenuDirective();
6+
expect(directive).toBeTruthy();
7+
});
8+
});
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import type { InputSignal, ModelSignal } from '@angular/core';
2+
import { Directive, model } from '@angular/core';
3+
import { MenuDirective } from 'tableau-ui-angular/menu';
4+
5+
@Directive({
6+
selector: 'ng-template[buttonMenu]',
7+
standalone: false,
8+
exportAs: 'buttonMenu'
9+
})
10+
export class ButtonMenuDirective extends MenuDirective {
11+
// #region Inputs
12+
override readonly $menuContainerCss: ModelSignal<Record<string, string>> = model<Record<string, string>>(
13+
{
14+
pointerEvents: 'none',
15+
marginTop: '-1px',
16+
background: 'transparent',
17+
},
18+
{
19+
alias: 'menuContainerCss',
20+
},
21+
);
22+
23+
override readonly $closeOnBackdropClick: InputSignal<boolean> = model(true, {
24+
alias: 'closeOnBackdropClick',
25+
});
26+
override readonly $minWidth: ModelSignal<string | 'fit-content' | 'parentWidth'> = model('parentWidth', {
27+
alias: 'minWidth',
28+
});
29+
override readonly $width: ModelSignal<string | 'fit-content' | 'parentWidth'> = model('fit-content', {
30+
alias: 'width',
31+
});
32+
33+
// #endregion Inputs
34+
35+
36+
}

0 commit comments

Comments
 (0)