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(dialog): open dynamic component in dialog #180

Merged
merged 1 commit into from
Mar 6, 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
Original file line number Diff line number Diff line change
@@ -0,0 +1,239 @@
import { Component, HostBinding, inject } from '@angular/core';
import { lucideCheck } from '@ng-icons/lucide';
import { HlmButtonDirective } from '@spartan-ng/ui-button-helm';
import { BrnDialogRef, injectBrnDialogContext } from '@spartan-ng/ui-dialog-brain';
import {
HlmDialogDescriptionDirective,
HlmDialogHeaderComponent,
HlmDialogService,
HlmDialogTitleDirective,
} from '@spartan-ng/ui-dialog-helm';
import { HlmIconComponent, provideIcons } from '@spartan-ng/ui-icon-helm';
import { HlmTableComponent, HlmTdComponent, HlmThComponent, HlmTrowComponent } from '@spartan-ng/ui-table-helm';

type ExampleUser = {
name: string;
email: string;
phone: string;
};

@Component({
selector: 'spartan-dialog-dynamic-component-preview',
standalone: true,
imports: [HlmButtonDirective],
template: `
<button hlmBtn (click)="openDynamicComponent()">Select User</button>
`,
})
export class DialogDynamicComponentPreviewComponent {
private readonly _hlmDialogService = inject(HlmDialogService);

private readonly _users: ExampleUser[] = [
{
name: 'Helena Chambers',
email: 'helenachambers@chorizon.com',
phone: '+1 (812) 588-3759',
},
{
name: 'Josie Crane',
email: 'josiecrane@hinway.com',
phone: '+1 (884) 523-3324',
},
{
name: 'Lou Hartman',
email: 'louhartman@optyk.com',
phone: '+1 (912) 479-3998',
},
{
name: 'Lydia Zimmerman',
email: 'lydiazimmerman@ultrasure.com',
phone: '+1 (944) 511-2111',
},
];

public openDynamicComponent() {
const dialogRef = this._hlmDialogService.open(SelectUserComponent, {
context: {
users: this._users,
},
contentClass: 'sm:!max-w-[750px]',
});

dialogRef.closed$.subscribe((user) => {
if (user) {
console.log('Selected user:', user);
}
});
}
}

@Component({
selector: 'dynamic-content',
standalone: true,
imports: [
HlmDialogHeaderComponent,
HlmDialogTitleDirective,
HlmDialogDescriptionDirective,
HlmTableComponent,
HlmThComponent,
HlmTrowComponent,
HlmTdComponent,
HlmButtonDirective,
HlmIconComponent,
],
providers: [provideIcons({ lucideCheck })],
template: `
<hlm-dialog-header>
<h3 hlmDialogTitle>Select user</h3>
<p hlmDialogDescription>Click a row to select a user.</p>
</hlm-dialog-header>

<hlm-table>
<hlm-trow>
<hlm-th class="w-44">Name</hlm-th>
<hlm-th class="w-60">Email</hlm-th>
<hlm-th class="w-48">Phone</hlm-th>
</hlm-trow>
@for (user of users; track user.name) {
<button class="text-left" (click)="selectUser(user)">
<hlm-trow>
<hlm-td truncate class="w-44 font-medium">{{ user.name }}</hlm-td>
<hlm-td class="w-60">{{ user.email }}</hlm-td>
<hlm-td class="w-48">{{ user.phone }}</hlm-td>
</hlm-trow>
</button>
}
</hlm-table>
`,
})
class SelectUserComponent {
@HostBinding('class') private readonly _class: string = 'flex flex-col gap-4';

private readonly _dialogRef = inject<BrnDialogRef<ExampleUser>>(BrnDialogRef);
private readonly _dialogContext = injectBrnDialogContext<{ users: ExampleUser[] }>();

protected readonly users = this._dialogContext.users;

public selectUser(user: ExampleUser) {
this._dialogRef.close(user);
}
}

export const dynamicComponentCode = `
import {
HlmDialogDescriptionDirective,
HlmDialogHeaderComponent,
HlmDialogService,
HlmDialogTitleDirective,
} from '@spartan-ng/ui-dialog-helm';
import { HlmIconComponent, provideIcons } from '@spartan-ng/ui-icon-helm';
import { HlmTableComponent, HlmTdComponent, HlmThComponent, HlmTrowComponent } from '@spartan-ng/ui-table-helm';

type ExampleUser = {
name: string;
email: string;
phone: string;
};

@Component({
selector: 'spartan-dialog-dynamic-component-preview',
standalone: true,
imports: [HlmButtonDirective],
template: \`
<button hlmBtn (click)="openDynamicComponent()">Select User</button>
\`,
})
export class DialogDynamicComponentPreviewComponent {
private readonly _hlmDialogService = inject(HlmDialogService);

private readonly _users: ExampleUser[] = [
{
name: 'Helena Chambers',
email: 'helenachambers@chorizon.com',
phone: '+1 (812) 588-3759',
},
{
name: 'Josie Crane',
email: 'josiecrane@hinway.com',
phone: '+1 (884) 523-3324',
},
{
name: 'Lou Hartman',
email: 'louhartman@optyk.com',
phone: '+1 (912) 479-3998',
},
{
name: 'Lydia Zimmerman',
email: 'lydiazimmerman@ultrasure.com',
phone: '+1 (944) 511-2111',
},
];

public openDynamicComponent() {
const dialogRef = this._hlmDialogService.open(SelectUserComponent, {
context: {
users: this._users,
},
contentClass: 'sm:!max-w-[750px]',
});

dialogRef.closed$.subscribe((user) => {
if (user) {
console.log('Selected user:', user);
}
});
}
}

@Component({
selector: 'dynamic-content',
standalone: true,
imports: [
HlmDialogHeaderComponent,
HlmDialogTitleDirective,
HlmDialogDescriptionDirective,
HlmTableComponent,
HlmThComponent,
HlmTrowComponent,
HlmTdComponent,
HlmButtonDirective,
HlmIconComponent,
],
providers: [provideIcons({ lucideCheck })],
template: \`
<hlm-dialog-header>
<h3 hlmDialogTitle>Select user</h3>
<p hlmDialogDescription>Click a row to select a user.</p>
</hlm-dialog-header>

<hlm-table>
<hlm-trow>
<hlm-th class="w-44">Name</hlm-th>
<hlm-th class="w-60">Email</hlm-th>
<hlm-th class="w-48">Phone</hlm-th>
</hlm-trow>
@for (user of users; track user.name) {
<button class="text-left" (click)="selectUser(user)">
<hlm-trow>
<hlm-td truncate class="w-44 font-medium">{{ user.name }}</hlm-td>
<hlm-td class="w-60">{{ user.email }}</hlm-td>
<hlm-td class="w-48">{{ user.phone }}</hlm-td>
</hlm-trow>
</button>
}
</hlm-table>
\`,
})
class SelectUserComponent {
@HostBinding('class') private readonly _class: string = 'flex flex-col gap-4';

private readonly _dialogRef = inject<BrnDialogRef<ExampleUser>>(BrnDialogRef);
private readonly _dialogContext = injectBrnDialogContext<{ users: ExampleUser[] }>();

protected readonly users = this._dialogContext.users;

public selectUser(user: ExampleUser) {
this._dialogRef.close(user);
}
}
`;
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import { TabsCliComponent } from '../../../../shared/layout/tabs-cli.component';
import { TabsComponent } from '../../../../shared/layout/tabs.component';
import { metaWith } from '../../../../shared/meta/meta.util';
import { DialogContextMenuPreviewComponent, contextMenuCode } from './dialog-context-menu.preview';
import { DialogDynamicComponentPreviewComponent, dynamicComponentCode } from './dialog-dynamic-component.preview';
import { DialogPreviewComponent, defaultCode, defaultImports, defaultSkeleton } from './dialog.preview';

export const routeMeta: RouteMeta = {
Expand Down Expand Up @@ -49,6 +50,7 @@ export const routeMeta: RouteMeta = {
DialogPreviewComponent,
DialogPreviewComponent,
DialogContextMenuPreviewComponent,
DialogDynamicComponentPreviewComponent,
HlmAlertDirective,
HlmAlertDescriptionDirective,
HlmIconComponent,
Expand Down Expand Up @@ -116,6 +118,20 @@ export const routeMeta: RouteMeta = {
<spartan-code secondTab [code]="contextMenuCode" />
</spartan-tabs>

<spartan-section-sub-heading id="dynamic-component">Dynamic Component</spartan-section-sub-heading>
<p class="${hlmP} mb-6">
You can dynamically open a dialog with a component rendered as the content. The dialog context can be injected
into the dynamic component using the provided
<code class="${hlmCode}">injectBrnDialogContext</code>
function.
</p>
<spartan-tabs firstTab="Preview" secondTab="Code">
<div spartanCodePreview firstTab>
<spartan-dialog-dynamic-component-preview />
</div>
<spartan-code secondTab [code]="dynamicComponentCode" />
</spartan-tabs>

<spartan-page-bottom-nav>
<spartan-page-bottom-nav-link href="dropdown-menu" label="Dropdown Menu" />
<spartan-page-bottom-nav-link direction="previous" href="data-table" label="Data Table" />
Expand All @@ -129,4 +145,5 @@ export default class DialogPageComponent {
protected readonly defaultSkeleton = defaultSkeleton;
protected readonly defaultImports = defaultImports;
protected readonly contextMenuCode = contextMenuCode;
protected readonly dynamicComponentCode = dynamicComponentCode;
}
107 changes: 107 additions & 0 deletions apps/ui-storybook-e2e/src/integration/dialog/dialog.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,4 +98,111 @@ describe('dialog--default', () => {
cy.findAllByText(/edit profile/i).should('have.focus');
});
});

describe('nested dialog', () => {
beforeEach(() => {
cy.visit('/iframe.html?id=dialog--nested-dialog');
cy.injectAxe();
});

it('click on trigger should open the first dialog, click on button inside first dialog should open a nested dialog, click on button inside nested dialog closes nested dialog', () => {
cy.findByText(/open dialog/i).should('have.attr', 'aria-haspopup', 'dialog');
cy.findByText(/open dialog/i).click();

cy.get('#brn-dialog-0');
cy.get('#brn-dialog-0').should('have.attr', 'aria-labelledby', 'brn-dialog-title-0');
cy.get('#brn-dialog-0').should('have.attr', 'aria-labelledby', 'brn-dialog-title-0');
cy.get('#brn-dialog-0').should('have.attr', 'aria-modal', 'true');
cy.get('#brn-dialog-0').should('have.attr', 'tabindex', '-1');

cy.findByText(/first dialog/i);
cy.findByText(/open nested dialog/i).should('have.attr', 'aria-haspopup', 'dialog');
cy.findByText(/open nested dialog/i).click();

cy.get('#brn-dialog-1');
cy.get('#brn-dialog-1').should('have.attr', 'aria-labelledby', 'brn-dialog-title-1');
cy.get('#brn-dialog-1').should('have.attr', 'aria-labelledby', 'brn-dialog-title-1');
cy.get('#brn-dialog-1').should('have.attr', 'aria-modal', 'true');
cy.get('#brn-dialog-1').should('have.attr', 'tabindex', '-1');

cy.get('#brn-dialog-1')
.findByText(/close nested dialog/i)
.click();

cy.wait(100);

cy.get('.cdk-overlay-backdrop').click({ force: true });

cy.findAllByText(/open dialog/i).should('have.length', 1);
cy.findAllByText(/open dialog/i).should('have.focus');
});
});
});

describe('dialog--dynamic-component', () => {
describe('dynamic-component', () => {
beforeEach(() => {
cy.visit('/iframe.html?id=dialog--dynamic-component');
cy.injectAxe();
});

it('click on button should open dyanmic component, click on close should close, click outside should close', () => {
cy.findAllByText(/select user/i).click();
cy.findByRole('dialog');
cy.findByRole('dialog').should('have.attr', 'aria-labelledby', 'brn-dialog-title-0');
cy.findByRole('dialog').should('have.attr', 'aria-labelledby', 'brn-dialog-title-0');
cy.findByRole('dialog').should('have.attr', 'aria-modal', 'true');
cy.findByRole('dialog').should('have.attr', 'tabindex', '-1');
cy.get('dynamic-content');

// close on click close button
cy.findByRole('dialog').get('hlm-icon').click();
cy.findAllByText(/select user/i).should('have.length', 1);
cy.findAllByText(/select user/i).should('have.focus');
cy.findByText(/select user/i).click();

// close on click backdrop
cy.get('dynamic-content');
cy.get('.cdk-overlay-backdrop').click({ force: true });
cy.findAllByText(/select user/i).should('have.length', 1);
cy.findAllByText(/select user/i).should('have.focus');
});
});

describe('nested dialog', () => {
beforeEach(() => {
cy.visit('/iframe.html?id=dialog--nested-dynamic-component');
cy.injectAxe();
});

it('click on trigger should open the first dialog, click on button inside first dialog should open a nested dialog, click on button inside nested dialog closes nested dialog', () => {
cy.findByText(/open dialog/i).click();

cy.get('#brn-dialog-0');
cy.get('#brn-dialog-0').should('have.attr', 'aria-labelledby', 'brn-dialog-title-0');
cy.get('#brn-dialog-0').should('have.attr', 'aria-labelledby', 'brn-dialog-title-0');
cy.get('#brn-dialog-0').should('have.attr', 'aria-modal', 'true');
cy.get('#brn-dialog-0').should('have.attr', 'tabindex', '-1');

cy.findByText(/first dialog/i);
cy.findByText(/open nested dialog/i).click();

cy.get('#brn-dialog-1');
cy.get('#brn-dialog-1').should('have.attr', 'aria-labelledby', 'brn-dialog-title-1');
cy.get('#brn-dialog-1').should('have.attr', 'aria-labelledby', 'brn-dialog-title-1');
cy.get('#brn-dialog-1').should('have.attr', 'aria-modal', 'true');
cy.get('#brn-dialog-1').should('have.attr', 'tabindex', '-1');

cy.get('#brn-dialog-1')
.findByText(/close nested dialog/i)
.click();

cy.wait(100);

cy.get('.cdk-overlay-backdrop').click({ force: true });

cy.findAllByText(/open dialog/i).should('have.length', 1);
cy.findAllByText(/open dialog/i).should('have.focus');
});
});
});
Loading
Loading