Skip to content

Migrate all toolbar components to standalone and use the new syntax #32257

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

Closed
wants to merge 19 commits into from
Closed
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
b26549f
refactor(modules): Replace DotToolbarModule with DotToolbarComponent …
nicobytes May 27, 2025
75fa8e9
refactor(dot-dropdown): Enhance dropdown component with signals and c…
nicobytes May 27, 2025
dc50347
Merge branch '32256-new-angular-syntax-in-dottoolbarcomponent' of git…
nicobytes May 28, 2025
3c53e33
refactor(dot-dropdown): Update dropdown component to utilize signals …
nicobytes May 28, 2025
6832eed
chore(dependencies): Update ng-mocks to version 14.13.5 for improved …
nicobytes May 29, 2025
cef84ac
refactor(dot-crumbtrail): Remove DotCrumbtrailModule as part of compo…
nicobytes May 29, 2025
1247a9d
refactor(components): Update templates to utilize new Angular syntax …
nicobytes May 29, 2025
f796e66
Merge branch 'main' into 32256-new-angular-syntax-in-dottoolbarcomponent
nicobytes May 29, 2025
5c7f066
refactor(dot-notifications): Reorganize imports for improved clarity …
nicobytes May 29, 2025
52dda86
Merge branch '32256-new-angular-syntax-in-dottoolbarcomponent' of git…
nicobytes May 29, 2025
f83e2cf
refactor(dot-notifications): Update button import and fix template sy…
nicobytes May 29, 2025
8e9f58a
refactor(dot-notifications): Update button import and fix template sy…
nicobytes May 29, 2025
faa14e7
Update core-web/apps/dotcms-ui/src/app/view/components/_common/dot-dr…
nicobytes May 29, 2025
af482c0
refactor(components): Update templates to utilize new Angular syntax …
nicobytes May 29, 2025
acd9298
chore(dependencies): downgrade ng-mocks to version 14.12.1 to resolve…
nicobytes May 30, 2025
b8c551b
sync with master
nicobytes Jun 26, 2025
ad55a5e
chore(dotcms-ui): Refactor DotToolbar and SharedModule components
nicobytes Jun 26, 2025
9704f2a
Merge branch 'main' into 32256-new-angular-syntax-in-dottoolbarcomponent
nicobytes Jun 26, 2025
b58834f
refactor(dotcms-ui): Replace DotDropdownComponent with DotDropdownMod…
nicobytes Jun 26, 2025
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
Prev Previous commit
Next Next commit
refactor(dot-dropdown): Update dropdown component to utilize signals …
…and computed properties for improved state management and accessibility
  • Loading branch information
nicobytes committed May 28, 2025
commit 3c53e33fb36af671b29a6d80da978496f9cc227f
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
@let disabled = $disabledState();
@let show = $show();
@let icon = $icon();
@let title = $title();
Expand All @@ -9,7 +8,7 @@
@if (icon) {
<button
(click)="onToggle()"
[disabled]="disabled"
[disabled]="$disabledIcon()"
[icon]="icon"
class="p-button-rounded p-button-text"
pButton
Expand All @@ -20,7 +19,7 @@
<button
(click)="onToggle()"
[label]="title"
[disabled]="disabled"
[disabled]="$disabled()"
pButton
icon="pi pi-chevron-down"
iconPos="right"
Expand Down
Original file line number Diff line number Diff line change
@@ -1,139 +1,199 @@
import { Component, DebugElement } from '@angular/core';
import { ComponentFixture, waitForAsync } from '@angular/core/testing';
import { By } from '@angular/platform-browser';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { Spectator, byTestId, createComponentFactory } from '@ngneat/spectator';

import { ButtonModule } from 'primeng/button';
import { DotDropdownComponent } from './dot-dropdown.component';

import { DOTTestBed } from '@dotcms/app/test/dot-test-bed';
describe('DotDropdownComponent', () => {
let spectator: Spectator<DotDropdownComponent>;
const createComponent = createComponentFactory(DotDropdownComponent);

import { DotDropdownComponent } from './dot-dropdown.component';
beforeEach(() =>(spectator = createComponent({
detectChanges: false
})));

@Component({
selector: 'dot-test-host-component',
template: `
<dot-dropdown-component
[icon]="icon"
[title]="title"
[disabled]="disabled"></dot-dropdown-component>
`
})
class DotTestHostComponent {
disabled: boolean;
icon: string;
title: string;

constructor() {
this.icon = 'icon';
this.title = 'test';
}
}

function executeEnabled(
elem: DebugElement,
hostFixture: ComponentFixture<DotTestHostComponent>,
de: DebugElement,
comp: DotDropdownComponent
) {
elem.nativeElement.click();
hostFixture.detectChanges();
const content = de.query(By.css('.dropdown-content'));
expect(content).toBeTruthy();
expect(elem).toBeTruthy();
expect(comp.toggle.emit).toHaveBeenCalledWith(true);
}

function executeDisabled(elem: DebugElement, de: DebugElement) {
elem.nativeElement.click();
const content = de.query(By.css('.dropdown-content'));
expect(content).toBeFalsy();
expect(elem).toBeTruthy();
}
describe('Enabled', () => {
it('should display icon button and emit toggle event when clicked', () => {
spectator.setInput('icon', 'test');
spectator.setInput('disabled', false);
spectator.detectChanges();

describe('DotDropdownComponent', () => {
let hostFixture: ComponentFixture<DotTestHostComponent>;
let hostDe: DebugElement;
let hostComp: DotTestHostComponent;
const iconBtn = spectator.query<HTMLButtonElement>(byTestId('icon-button'));
const spyToggle = spyOn(spectator.component.toggle, 'emit');
const spyWasOpen = spyOn(spectator.component.wasOpen, 'emit');

spectator.click(iconBtn);

let comp: DotDropdownComponent;
let de: DebugElement;
const content = spectator.query('.dropdown-content');

beforeEach(waitForAsync(() => {
DOTTestBed.configureTestingModule({
declarations: [DotDropdownComponent, DotTestHostComponent],
imports: [BrowserAnimationsModule, ButtonModule]
expect(content).toBeTruthy();
expect(iconBtn).toBeTruthy();
expect(iconBtn.disabled).toBeFalsy();
expect(spyToggle).toHaveBeenCalledWith(true);
expect(spyWasOpen).toHaveBeenCalled();
});

hostFixture = DOTTestBed.createComponent(DotTestHostComponent);
hostComp = hostFixture.debugElement.componentInstance;
hostDe = hostFixture.debugElement;
it('should display title button and emit toggle event when clicked', () => {
spectator.setInput('title', 'test');
spectator.setInput('disabled', false);
spectator.detectChanges();

de = hostDe.query(By.css('dot-dropdown-component'));
comp = de.componentInstance;
}));
const titleBtn = spectator.query<HTMLButtonElement>(byTestId('title-button'));
const spyToggle = spyOn(spectator.component.toggle, 'emit');
const spyWasOpen = spyOn(spectator.component.wasOpen, 'emit');

describe('Enabled', () => {
let button: DebugElement;
let titleButton: DebugElement;

beforeEach(() => {
spyOn(comp.toggle, 'emit');
hostComp.disabled = false;
hostFixture.detectChanges();
button = de.query(By.css('[data-testid="icon-button"]'));
titleButton = de.query(By.css('[data-testid="title-button"]'));
spectator.click(titleBtn);

const content = spectator.query('.dropdown-content');

expect(content).toBeTruthy();
expect(titleBtn).toBeTruthy();
expect(titleBtn.disabled).toBeFalsy();
expect(spyToggle).toHaveBeenCalledWith(true);
expect(spyWasOpen).toHaveBeenCalled();
});
});

describe('Disabled', () => {
it('should disable icon button and not emit when disabled', () => {
spectator.setInput('icon', 'test');
spectator.setInput('disabled', true);
spectator.detectChanges();

const iconBtn = spectator.query<HTMLButtonElement>(byTestId('icon-button'));
const spyToggle = spyOn(spectator.component.toggle, 'emit');

expect(iconBtn.disabled).toBeTruthy();

it(`should dot-icon button be displayed & emit`, () => {
executeEnabled(button, hostFixture, de, comp);
expect(button.attributes.disabled).not.toBeDefined();
spectator.click(iconBtn);
expect(spyToggle).not.toHaveBeenCalled();
});

it(`should title button be displayed & emit`, () => {
executeEnabled(titleButton, hostFixture, de, comp);
it('should disable title button and not emit when disabled', () => {
spectator.setInput('title', 'test');
spectator.setInput('disabled', true);
spectator.detectChanges();

const titleBtn = spectator.query<HTMLButtonElement>(byTestId('title-button'));
const spyToggle = spyOn(spectator.component.toggle, 'emit');

expect(titleBtn.disabled).toBeTruthy();

spectator.click(titleBtn);
expect(spyToggle).not.toHaveBeenCalled();
});
});

describe('Disabled', () => {
let button: DebugElement;
let titleButton: DebugElement;

beforeEach(() => {
spyOn(comp.toggle, 'emit');
hostComp.disabled = true;
hostFixture.detectChanges();
button = de.query(By.css('[data-testid="icon-button"]'));
titleButton = de.query(By.css('[data-testid="title-button"]'));
describe('Dropdown behavior', () => {
it('should show and hide the dropdown dialog', () => {
spectator.detectChanges();

expect(spectator.query('.dropdown-content')).toBeFalsy();

spectator.component.onToggle();
spectator.detectChanges();

expect(spectator.query('.dropdown-content')).toBeTruthy();

spectator.component.closeIt();
spectator.detectChanges();

expect(spectator.query('.dropdown-content')).toBeFalsy();
});

it(`should dot-icon button not be displayed --> null`, () => {
executeDisabled(button, de);
expect(button.componentInstance.disabled).toBe(true);
it('should emit shutdown event when closing dropdown', () => {
const spyShutdown = spyOn(spectator.component.shutdown, 'emit');

spectator.component.onToggle(); // Open
spectator.component.onToggle(); // Close

expect(spyShutdown).toHaveBeenCalled();
});

it(`should title button not be displayed & not emit`, () => {
executeDisabled(titleButton, de);
expect(comp.toggle.emit).not.toHaveBeenCalled();
it('should show the mask when dropdown is open', () => {
spectator.detectChanges();
spectator.component.onToggle();
spectator.detectChanges();

const mask = spectator.query('.dot-mask');

expect(mask).toBeTruthy();
});
});

it(`should hide the dropdown dialog`, () => {
comp.closeIt();
expect(comp.show).toBe(false);
it('should hide the mask when dropdown is closed', () => {
spectator.detectChanges();
spectator.component.onToggle();
spectator.detectChanges();

expect(spectator.query('.dot-mask')).toBeTruthy();

spectator.component.closeIt();
spectator.detectChanges();

expect(spectator.query('.dot-mask')).toBeFalsy();
});

it('should close dropdown when clicking on mask', () => {
spectator.detectChanges();
spectator.component.onToggle();
spectator.detectChanges();

const mask = spectator.query('.dot-mask');
const spyToggle = spyOn(spectator.component.toggle, 'emit');

spectator.click(mask);

expect(spyToggle).toHaveBeenCalledWith(false);
});
});

it('shold show the mask', () => {
comp.show = true;
hostFixture.detectChanges();
const mask = de.query(By.css('.dot-mask'));
mask.nativeElement.click();
expect(mask).toBeTruthy();
describe('Computed properties', () => {
it('should compute $disabledIcon correctly when icon is present and disabled', () => {
spectator.setInput('icon', 'test');
spectator.setInput('disabled', true);
spectator.detectChanges();

expect(spectator.component.$disabledIcon()).toBeTruthy();
});

it('should compute $disabledIcon as false when no icon is present', () => {
spectator.setInput('disabled', true);
spectator.detectChanges();

expect(spectator.component.$disabledIcon()).toBeFalsy();
});

it('should compute $style with correct positioning', () => {
spectator.setInput('position', 'right');
spectator.detectChanges();

expect(spectator.component.$style()).toEqual({ right: '0' });
});

it('should compute $style with left positioning by default', () => {
spectator.detectChanges();

expect(spectator.component.$style()).toEqual({ left: '0' });
});
});

it('shold hide the mask', () => {
comp.show = false;
hostFixture.detectChanges();
const mask = de.query(By.css('.dot-mask'));
expect(mask).toBeFalsy();
describe('Click outside behavior', () => {
it('should close dropdown when clicking outside component', () => {
spectator.component.onToggle();
spectator.detectChanges();

expect(spectator.component.$show()).toBeTruthy();

// Simulate click outside
const outsideElement = document.createElement('div');
document.body.appendChild(outsideElement);

const clickEvent = new MouseEvent('click', { bubbles: true });
Object.defineProperty(clickEvent, 'target', { value: outsideElement });

spectator.component.handleClick(clickEvent);

expect(spectator.component.$show()).toBeFalsy();

document.body.removeChild(outsideElement);
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@ import {
inject,
input,
signal,
output
output,
ChangeDetectionStrategy
} from '@angular/core';

import { ButtonModule } from 'primeng/button';
Expand All @@ -29,6 +30,7 @@ import { ButtonModule } from 'primeng/button';
styleUrls: ['./dot-dropdown.component.scss'],
templateUrl: 'dot-dropdown.component.html',
standalone: true,
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [ButtonModule]
})
export class DotDropdownComponent {
Expand Down Expand Up @@ -86,7 +88,7 @@ export class DotDropdownComponent {
* Returns true only when both disabled is true AND an icon is present.
* @returns true if dropdown should be disabled with icon, false otherwise
*/
$disabledState = computed(() => {
$disabledIcon = computed(() => {
const icon = this.$icon();
const disabled = this.$disabled();

Expand Down Expand Up @@ -121,25 +123,19 @@ export class DotDropdownComponent {
/**
* Host listener that handles clicks outside the dropdown component.
* Automatically closes the dropdown when user clicks outside of it.
* Traverses the DOM tree to determine if click occurred inside component.
* Uses the native contains() method for efficient DOM checking.
*
* @param $event - The mouse click event from the document
* @param event - The mouse click event from the document
*/
@HostListener('document:click', ['$event'])
handleClick($event) {
let clickedComponent = $event.target;
let inside = false;
do {
if (clickedComponent === this.#elementRef.nativeElement) {
inside = true;
}

clickedComponent = clickedComponent.parentNode;
} while (clickedComponent);

if (!inside) {
this.$show.set(false);
handleClick(event: MouseEvent): void {
const target = event.target;

if (!target || this.#elementRef.nativeElement.contains(target)) {
return;
}

this.$show.set(false);
}

/**
Expand Down
Loading