Skip to content

Commit

Permalink
feat(workbench/dialog): support custom header and footer
Browse files Browse the repository at this point in the history
## Dialog Header
By default, the dialog displays the title and a close button in the header. Alternatively, the dialog supports the use of a custom header.

To provide a custom header, add an Angular template to the HTML of the dialog component and decorate it with the `wbDialogHeader` directive.

```html
<ng-template wbDialogHeader>
  <app-dialog-header/>
</ng-template>
```

## Dialog Footer
A dialog has a default footer that displays actions defined in the HTML of the dialog component. An action is an Angular template decorated with
the `wbDialogAction` directive. Multiple actions are supported, rendered in modeling order, and can be left- or right-aligned.

```html
<!-- Checkbox -->
<ng-template wbDialogAction align="start">
  <label>
    <input type="checkbox"/>
    Do not ask me again
  </label>
</ng-template>

<!-- OK Button -->
<ng-template wbDialogAction align="end">
  <button (click)="...">OK</button>
</ng-template>

<!-- Cancel Button -->
<ng-template wbDialogAction align="end">
  <button (click)="...">Cancel</button>
</ng-template>
```

Alternatively, the dialog supports the use of a custom footer. To provide a custom footer, add an Angular template to the HTML of the dialog component and
decorate it with the `wbDialogFooter` directive.

```html
<ng-template wbDialogFooter>
  <app-dialog-footer/>
</ng-template>
```

BREAKING CHANGE: Support for custom header and footer has introduced a breaking change.

To migrate:
- Type of `WorkbenchDialog.padding` is now `boolean`. Set to `false` to remove the padding, or set the CSS variable `--sci-workbench-dialog-padding` for a custom padding.
- `--sci-workbench-dialog-header-divider-color` CSS variable has been removed (no replacement).
  • Loading branch information
k-genov authored and danielwiehl committed Jan 18, 2024
1 parent 93682e7 commit 019a4d2
Show file tree
Hide file tree
Showing 31 changed files with 707 additions and 206 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
</section>

<section>
<sci-form-field label="Input">
<sci-form-field label="Inputs">
<sci-key-value-field [keyValueFormArray]="form.controls.options.controls.inputs" [addable]="true" [removable]="true" class="e2e-inputs"/>
</sci-form-field>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@
</ng-template>
<ng-template #panel_styles>
<sci-form-field label="Padding">
<input [formControl]="form.controls.styles.controls.padding" class="e2e-padding">
<sci-checkbox [formControl]="form.controls.styles.controls.padding" class="e2e-padding"></sci-checkbox>
</sci-form-field>
</ng-template>
</sci-accordion>
Expand All @@ -93,7 +93,13 @@
</sci-accordion>
</form>

<div class="buttons">
<ng-template wbDialogAction align="start">
<label>
<sci-checkbox [formControl]="form.controls.closeWithError" class="e2e-close-with-error"></sci-checkbox>
Close with error
</label>
</ng-template>

<ng-template wbDialogAction align="end">
<button (click)="onClose()" class="e2e-close" sci-primary>Close</button>
<button (click)="onCloseWithError()" class="e2e-close-with-error">Close (with error)</button>
</div>
</ng-template>
Original file line number Diff line number Diff line change
@@ -1,11 +1,9 @@
@use '@scion/components.internal/design' as sci-design;

:host {
display: block;
--ɵapp-dialog-page-height: initial;
--ɵapp-dialog-page-width: initial;

display: flex;
flex-direction: column;
height: var(--ɵapp-dialog-page-height);
width: var(--ɵapp-dialog-page-width);
overflow: hidden; // crop overflow when changing minimum size in end-to-end test
Expand All @@ -26,12 +24,10 @@
}
}
}
}

> div.buttons {
flex: none;
display: grid;
grid-template-columns: 1fr 1fr;
column-gap: .25em;
margin-top: 1em;
}
label {
display: flex;
align-items: center;
gap: .5em;
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
*/

import {Component, HostBinding, Input} from '@angular/core';
import {WorkbenchDialog} from '@scion/workbench';
import {WorkbenchDialog, WorkbenchDialogActionDirective} from '@scion/workbench';
import {FormGroup, NonNullableFormBuilder, ReactiveFormsModule} from '@angular/forms';
import {UUID} from '@scion/toolkit/uuid';
import {NgIf} from '@angular/common';
Expand All @@ -30,6 +30,7 @@ import {SciCheckboxComponent} from '@scion/components.internal/checkbox';
SciAccordionComponent,
SciAccordionItemDirective,
SciCheckboxComponent,
WorkbenchDialogActionDirective,
],
})
export class DialogPageComponent {
Expand All @@ -54,8 +55,9 @@ export class DialogPageComponent {
resizable: this._formBuilder.control(this.dialog.resizable),
}),
styles: new FormGroup({
padding: this._formBuilder.control(''),
padding: this._formBuilder.control(this.dialog.padding),
}),
closeWithError: this._formBuilder.control(false),
result: this._formBuilder.control(''),
});

Expand All @@ -77,11 +79,12 @@ export class DialogPageComponent {
}

public onClose(): void {
this.dialog.close(this.form.controls.result.value);
}

public onCloseWithError(): void {
this.dialog.closeWithError(this.form.controls.result.value);
if (this.form.controls.closeWithError.value) {
this.dialog.closeWithError(this.form.controls.result.value);
}
else {
this.dialog.close(this.form.controls.result.value);
}
}

private installPropertyUpdater(): void {
Expand Down
76 changes: 75 additions & 1 deletion docs/site/howto/how-to-open-dialog.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ const dialogService = inject(WorkbenchDialogService);
dialogService.open(MyDialogComponent);
```

### How to control the modality of a dialog
### How to set the modality of a dialog
A dialog can be view-modal or application-modal. A view-modal dialog blocks only a specific view, allowing the user to interact with other views. An application-modal dialog blocks the workbench by default, or the browser's viewport, if set in the global workbench settings.

By default, the calling context determines the modality of the dialog. If the dialog is opened from a view, only this view is blocked. To open the dialog with a different modality, specify the modality in the dialog options.
Expand Down Expand Up @@ -80,6 +80,37 @@ The dialog component can inject the `WorkbenchDialog` handle and set the title.
inject(WorkbenchDialog).title = 'My dialog title';
```

### How to contribute to the dialog footer
A dialog has a default footer that displays actions defined in the HTML of the dialog component. An action is an Angular template decorated with the `wbDialogAction` directive. Multiple actions are supported, rendered in modeling order, and can be left- or right-aligned.

```html
<!-- Checkbox -->
<ng-template wbDialogAction align="start">
<label>
<input type="checkbox"/>
Do not ask me again
</label>
</ng-template>

<!-- OK Button -->
<ng-template wbDialogAction align="end">
<button (click)="...">OK</button>
</ng-template>

<!-- Cancel Button -->
<ng-template wbDialogAction align="end">
<button (click)="...">Cancel</button>
</ng-template>
```

Alternatively, the dialog supports the use of a custom footer. To provide a custom footer, add an Angular template to the HTML of the dialog component and decorate it with the `wbDialogFooter` directive.

```html
<ng-template wbDialogFooter>
<app-dialog-footer/>
</ng-template>
```

### How to close the dialog
The dialog component can inject the `WorkbenchDialog` handle and close the dialog, optionally passing a result to the dialog opener.

Expand Down Expand Up @@ -116,6 +147,49 @@ inject(WorkbenchDialog).size.maxHeight = '900px';
inject(WorkbenchDialog).size.maxWidth = '700px';
```

### How to use a custom dialog header
By default, the dialog displays the title and a close button in the header. Alternatively, the dialog supports the use of a custom header. To provide a custom header, add an Angular template to the HTML of the dialog component and decorate it with the `wbDialogHeader` directive.
```html
<ng-template wbDialogHeader>
<app-dialog-header/>
</ng-template>
```

### How to change default dialog settings
The dialog component can inject the dialog handle `WorkbenchDialog` to interact with the dialog and change its default settings, such as making it non-closable, non-resizable, removing padding, and more.

```ts
const dialog = inject(WorkbenchDialog);
dialog.closable = false;
dialog.resizable = false;
dialog.padding = false;
```

### How to change the default look of a dialog
The following CSS variables can be set to customize the default look of a dialog.

- `--sci-workbench-dialog-padding`
- `--sci-workbench-dialog-header-height`
- `--sci-workbench-dialog-header-background-color`
- `--sci-workbench-dialog-title-font-family`
- `--sci-workbench-dialog-title-font-weight`
- `--sci-workbench-dialog-title-font-size`
- `--sci-workbench-dialog-title-align`

To style a specific dialog, associate the dialog with a CSS class that can be referenced in a CSS selector.

```ts
inject(WorkbenchDialogService).open(MyDialogComponent, {
cssClass: 'fancy'
});
```

```css
wb-dialog.fancy {
--sci-workbench-dialog-title-align: start;
}
```

[menu-how-to]: /docs/site/howto/how-to.md

[menu-home]: /README.md
Expand Down
85 changes: 37 additions & 48 deletions projects/scion/e2e-testing/src/dialog-page.po.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,70 +21,59 @@ export class DialogPagePO {

public readonly locator: Locator;
public readonly input: Locator;
private readonly _title: Locator;
private readonly _closeButton: Locator;
private readonly _closeWithErrorButton: Locator;
private readonly _dialogSizeAccordion: SciAccordionPO;
private readonly _contentSizeAccordion: SciAccordionPO;
private readonly _behaviorAccordion: SciAccordionPO;
private readonly _stylesAccordion: SciAccordionPO;
private readonly _returnValueAccordion: SciAccordionPO;

constructor(dialog: DialogPO) {
this.locator = dialog.locator.locator('app-dialog-page');
constructor(private _dialog: DialogPO) {
this.locator = this._dialog.locator.locator('app-dialog-page');
this.input = this.locator.locator('input.e2e-input');
this._title = this.locator.locator('input.e2e-title');
this._dialogSizeAccordion = new SciAccordionPO(this.locator.locator('sci-accordion.e2e-dialog-size'));
this._contentSizeAccordion = new SciAccordionPO(this.locator.locator('sci-accordion.e2e-content-size'));
this._behaviorAccordion = new SciAccordionPO(this.locator.locator('sci-accordion.e2e-behavior'));
this._stylesAccordion = new SciAccordionPO(this.locator.locator('sci-accordion.e2e-styles'));
this._returnValueAccordion = new SciAccordionPO(this.locator.locator('sci-accordion.e2e-return-value'));
this._closeButton = this.locator.locator('button.e2e-close');
this._closeWithErrorButton = this.locator.locator('button.e2e-close-with-error');
}

public getComponentInstanceId(): Promise<string> {
return this.locator.locator('input.e2e-component-instance-id').inputValue();
}

public async enterTitle(title: string): Promise<void> {
await this._title.fill(title);
await this.locator.locator('input.e2e-title').fill(title);
}

public async setClosable(closable: boolean): Promise<void> {
await this._behaviorAccordion.expand();
await new SciCheckboxPO(this._behaviorAccordion.itemLocator().locator('sci-checkbox.e2e-closable')).toggle(closable);
await this._behaviorAccordion.collapse();
const accordion = new SciAccordionPO(this.locator.locator('sci-accordion.e2e-behavior'));
await accordion.expand();
await new SciCheckboxPO(accordion.itemLocator().locator('sci-checkbox.e2e-closable')).toggle(closable);
await accordion.collapse();
}

public async setResizable(resizable: boolean): Promise<void> {
await this._behaviorAccordion.expand();
await new SciCheckboxPO(this._behaviorAccordion.itemLocator().locator('sci-checkbox.e2e-resizable')).toggle(resizable);
await this._behaviorAccordion.collapse();
const accordion = new SciAccordionPO(this.locator.locator('sci-accordion.e2e-behavior'));
await accordion.expand();
await new SciCheckboxPO(accordion.itemLocator().locator('sci-checkbox.e2e-resizable')).toggle(resizable);
await accordion.collapse();
}

public async enterDialogSize(size: WorkbenchDialogSize): Promise<void> {
await this._dialogSizeAccordion.expand();
await this._dialogSizeAccordion.itemLocator().locator('input.e2e-min-height').fill(size.minHeight ?? '');
await this._dialogSizeAccordion.itemLocator().locator('input.e2e-height').fill(size.height ?? '');
await this._dialogSizeAccordion.itemLocator().locator('input.e2e-max-height').fill(size.maxHeight ?? '');
await this._dialogSizeAccordion.itemLocator().locator('input.e2e-min-width').fill(size.minWidth ?? '');
await this._dialogSizeAccordion.itemLocator().locator('input.e2e-width').fill(size.width ?? '');
await this._dialogSizeAccordion.itemLocator().locator('input.e2e-max-width').fill(size.maxWidth ?? '');
await this._dialogSizeAccordion.collapse();
const accordion = new SciAccordionPO(this.locator.locator('sci-accordion.e2e-dialog-size'));
await accordion.expand();
await accordion.itemLocator().locator('input.e2e-min-height').fill(size.minHeight ?? '');
await accordion.itemLocator().locator('input.e2e-height').fill(size.height ?? '');
await accordion.itemLocator().locator('input.e2e-max-height').fill(size.maxHeight ?? '');
await accordion.itemLocator().locator('input.e2e-min-width').fill(size.minWidth ?? '');
await accordion.itemLocator().locator('input.e2e-width').fill(size.width ?? '');
await accordion.itemLocator().locator('input.e2e-max-width').fill(size.maxWidth ?? '');
await accordion.collapse();
}

public async enterContentSize(size: {width?: string; height?: string}): Promise<void> {
await this._contentSizeAccordion.expand();
await this._contentSizeAccordion.itemLocator().locator('input.e2e-height').fill(size.height ?? '');
await this._contentSizeAccordion.itemLocator().locator('input.e2e-width').fill(size.width ?? '');
await this._contentSizeAccordion.collapse();
const accordion = new SciAccordionPO(this.locator.locator('sci-accordion.e2e-content-size'));
await accordion.expand();
await accordion.itemLocator().locator('input.e2e-height').fill(size.height ?? '');
await accordion.itemLocator().locator('input.e2e-width').fill(size.width ?? '');
await accordion.collapse();
}

public async setPadding(padding: string): Promise<void> {
await this._stylesAccordion.expand();
await this._stylesAccordion.itemLocator().locator('input.e2e-padding').fill(padding);
await this._stylesAccordion.collapse();
public async setPadding(padding: boolean): Promise<void> {
const accordion = new SciAccordionPO(this.locator.locator('sci-accordion.e2e-styles'));
await accordion.expand();
await new SciCheckboxPO(accordion.itemLocator().locator('sci-checkbox.e2e-padding')).toggle(padding);
await accordion.collapse();
}

public async close(options?: {returnValue?: string; closeWithError?: boolean}): Promise<void> {
Expand All @@ -93,16 +82,16 @@ export class DialogPagePO {
}

if (options?.closeWithError) {
await this._closeWithErrorButton.click();
}
else {
await this._closeButton.click();
await new SciCheckboxPO(this._dialog.footer.locator('sci-checkbox.e2e-close-with-error')).toggle(true);
}

await this._dialog.footer.locator('button.e2e-close').click();
}

private async enterReturnValue(returnValue: string): Promise<void> {
await this._returnValueAccordion.expand();
await this._returnValueAccordion.itemLocator().locator('input.e2e-return-value').fill(returnValue);
await this._returnValueAccordion.collapse();
const accordion = new SciAccordionPO(this.locator.locator('sci-accordion.e2e-return-value'));
await accordion.expand();
await accordion.itemLocator().locator('input.e2e-return-value').fill(returnValue);
await accordion.collapse();
}
}
22 changes: 14 additions & 8 deletions projects/scion/e2e-testing/src/dialog.po.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,32 +16,34 @@ import {DomRect, fromRect} from './helper/testing.util';
*/
export class DialogPO {

private readonly _dialogPane: Locator;
private readonly _dialog: Locator;

public readonly header: Locator;
public readonly title: Locator;
public readonly closeButton: Locator;
public readonly resizeHandles: Locator;
public readonly footer: Locator;

public readonly contentScrollbars: {
vertical: Locator;
horizontal: Locator;
};

constructor(public readonly locator: Locator) {
this._dialogPane = this.locator.locator('div.e2e-dialog-pane');
this.header = this._dialogPane.locator('header.e2e-dialog-header');
this._dialog = this.locator.locator('div.e2e-dialog');
this.header = this._dialog.locator('header.e2e-dialog-header');
this.title = this.header.locator('div.e2e-title > span');
this.closeButton = this.header.locator('button.e2e-close');
this.resizeHandles = this._dialogPane.locator('div.e2e-resize-handle');
this.footer = this._dialog.locator('footer.e2e-dialog-footer');
this.resizeHandles = this._dialog.locator('div.e2e-resize-handle');
this.contentScrollbars = {
vertical: this._dialogPane.locator('sci-viewport.e2e-dialog-content sci-scrollbar.e2e-vertical'),
horizontal: this._dialogPane.locator('sci-viewport.e2e-dialog-content sci-scrollbar.e2e-horizontal'),
vertical: this._dialog.locator('sci-viewport.e2e-dialog-content sci-scrollbar.e2e-vertical'),
horizontal: this._dialog.locator('sci-viewport.e2e-dialog-content sci-scrollbar.e2e-horizontal'),
};
}

public async getDialogBoundingBox(): Promise<DomRect> {
return fromRect(await this._dialogPane.boundingBox());
return fromRect(await this._dialog.boundingBox());
}

public async getGlassPaneBoundingBox(): Promise<DomRect> {
Expand All @@ -52,8 +54,12 @@ export class DialogPO {
return fromRect(await this.header.boundingBox());
}

public async getFooterBoundingBox(): Promise<DomRect> {
return fromRect(await this.footer.boundingBox());
}

public async getDialogBorderWidth(): Promise<number> {
return this._dialogPane.evaluate((element: HTMLElement) => parseInt(getComputedStyle(element).borderWidth, 10));
return this._dialog.locator('div.e2e-dialog-box').evaluate((element: HTMLElement) => parseInt(getComputedStyle(element).borderWidth, 10));
}

public async close(options?: {timeout?: number}): Promise<void> {
Expand Down
Loading

0 comments on commit 019a4d2

Please sign in to comment.