Skip to content

Commit

Permalink
feat(dialog): open dynamic component in dialog (#180)
Browse files Browse the repository at this point in the history
refactor(dialog): working on new dialog approach

refactor(dialog): refactor dialog component to use global dialog service

refactor(dialog): remove provider function for BrnDialog

feat(dialog): add HlmDialogService for opening components in a dialog dynamically

chore: add dynamic dialog component section to docs

chore: add note about injectDialogContext

refactor(dialog): dialog overlay class names and dialog options as reusable defaults

chore: add story for dialog dynamic components

refactor(dialog): make HlmDialogService options optional (defaults are provided)

refactor(dialog): continue support for injectBrnDialogCtx (deprecated)

refactor(dialog): move (click) handlers back into metadata host

refactor(dialog): prefer early returns

refactor(dialog): injectDialogContext -> injectBrnDialogContext

chore: add nested dialog stories and e2e tests

refactor(dialog): use input function in brnDialogTrigger

refactor(dialog): use optional chaining in dialog component

refactor(dialog): _id() renamed to id(), use in inheriting classes

refactor(popover): change ariaDescribedBy, ariaLabelledBy to empty string so not assigned defaults

refactor(dialog): use correct member names after merge
  • Loading branch information
ty-ler committed Mar 6, 2024
1 parent d9144da commit d4ffb47
Show file tree
Hide file tree
Showing 28 changed files with 1,052 additions and 248 deletions.
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

0 comments on commit d4ffb47

Please sign in to comment.