Skip to content

Commit

Permalink
feat(ui): create a collapse breadcrumb (#29379)
Browse files Browse the repository at this point in the history
### Parent Issue

#29358

### Proposed Changes

* Create new component named DotCollapseBreadcrumbComponent
* Create DotCollapseBreadcrumbComponent Storybook
* Create basic functionality to collapse items based on maxItems input
* Fix bug with dot-crumbtrail scope styles


### Checklist
- [x] Tests
- [x] Translations
- [x] Security Implications Contemplated (add notes if applicable)

### Goal

Create a reusable breadcrumb component in the @dotcms/ui package that
extends the official PrimeNG
[breadcrumb](https://www.primefaces.org/primeng-v15-lts/breadcrumb) by
adding collapsible items.

### References

<img width="675" alt="Screenshot 2024-07-10 at 12 16 04 PM"
src="https://github.com/dotCMS/core/assets/1909643/4e7c60f6-91ef-47ec-b8c4-68c96b8e3d01">
<img width="1022" alt="Screenshot 2024-07-10 at 12 16 12 PM"
src="https://github.com/dotCMS/core/assets/1909643/dfbd7d37-d2f2-4b26-9ead-27228d6e0795">
<img width="982" alt="Screenshot 2024-07-10 at 12 16 19 PM"
src="https://github.com/dotCMS/core/assets/1909643/838c292d-b993-41c1-a7bf-7434abe2707d">

### Result


![Screenshot 2024-07-29 at 3 29
45 PM](https://github.com/user-attachments/assets/f8d526c9-813f-4cbc-8f8e-02b4391f165b)

![Screenshot 2024-07-29 at 3 29
56 PM](https://github.com/user-attachments/assets/a3032765-da68-44da-b65e-b5c6acaa6c54)

![Screenshot 2024-07-29 at 3 30
08 PM](https://github.com/user-attachments/assets/169bb329-e951-4f25-9429-845de7e17d92)
  • Loading branch information
nicobytes authored Jul 30, 2024
1 parent 0663736 commit 4a03b35
Show file tree
Hide file tree
Showing 10 changed files with 359 additions and 47 deletions.
10 changes: 10 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,16 @@ node_modules/
/core-web/target
/core-web/~/dotcms/*

# Yarn
/core-web/.yarn/*
/core-web/.pnp.*
/core-web/.pnp**
!/core-web/.yarn/cache
!/core-web/.yarn/releases
!/core-web/.yarn/plugins
!/core-web/.yarn/sdks
!/core-web/.yarn/versions

# misc
/core-web/.nx
/core-web/.angular/cache
Expand Down
1 change: 1 addition & 0 deletions core-web/.prettierignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
/target
/tmp
/tools
/.yarn

package.json
package-lock.json
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
@use "variables" as *;

::ng-deep .p-breadcrumb {
::ng-deep dot-crumbtrail .p-breadcrumb {
ul li {
.p-menuitem-link[href] {
.p-menuitem-text {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
/* eslint-disable no-console */
import {
Meta,
StoryObj,
moduleMetadata,
componentWrapperDecorator,
argsToTemplate,
applicationConfig
} from '@storybook/angular';

import { importProvidersFrom } from '@angular/core';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';

import { MenuItem } from 'primeng/api';
import { ToastModule } from 'primeng/toast';

import { DotCollapseBreadcrumbComponent } from '@dotcms/ui';

type Args = DotCollapseBreadcrumbComponent & {
model: MenuItem[];
maxItems: number;
};

const meta: Meta<Args> = {
title: 'DotCMS/Menu/DotCollapseBreadcrumb',
component: DotCollapseBreadcrumbComponent,
decorators: [
applicationConfig({
providers: [importProvidersFrom(BrowserAnimationsModule)]
}),
moduleMetadata({
imports: [BrowserAnimationsModule, ToastModule]
}),
componentWrapperDecorator(
(story) =>
`<div class="card flex justify-content-center w-50rem h-10rem relative">${story}</div>`
)
],
parameters: {
layout: 'centered',
docs: {
description: {
component:
'Breadcrumb provides contextual information about page hierarchy.: https://primefaces.org/primeng/showcase/#/breadcrumb'
}
}
},
args: {
maxItems: 4,
model: [
{ label: 'Electronics', command: console.log },
{ label: 'Computer', command: console.log },
{ label: 'Accessories', command: console.log },
{ label: 'Keyboard', command: console.log },
{ label: 'Wireless', command: console.log }
]
},
argTypes: {
model: {
description: 'Menu items to display'
},
maxItems: {
description: 'Max items to display',
control: { type: 'number' }
}
},
render: (args: Args) => {
return {
props: {
...args
},
template: `<dot-collapse-breadcrumb ${argsToTemplate(args)} />`
};
}
};

export default meta;

type Story = StoryObj<Args>;

export const Default: Story = {};
1 change: 1 addition & 0 deletions core-web/libs/ui/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ export * from './lib/components/dot-menu/dot-menu.component';
export * from './lib/components/dot-action-menu-button/dot-action-menu-button.component';
export * from './lib/components/dot-ai-image-prompt/ai-image-prompt.component';
export * from './lib/components/dot-ai-image-prompt/ai-image-prompt.store';
export * from './lib/components/dot-collapse-breadcrumb/dot-collapse-breadcrumb.component';

// Directives
export * from './lib/dot-field-required/dot-field-required.directive';
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<div class="flex align-items-center">
@if ($isCollapsed()) {
<div class="flex align-items-center">
<div>
<p-menu #menu [model]="$itemsToHide()" [popup]="true" appendTo="body" />
<p-button
(click)="menu.toggle($event)"
icon="pi pi-ellipsis-h"
type="button"
data-testid="btn-collapse"
styleClass="p-button-rounded p-button-text p-button-sm outline-none" />
</div>
<div class="mx-2">
<ChevronRightIcon />
</div>
</div>
}
<p-breadcrumb [model]="$itemsToShow()" (onItemClick)="onItemClick.emit($event)" />
</div>
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
import { byTestId, createRoutingFactory, Spectator } from '@ngneat/spectator';

import { ActivatedRoute } from '@angular/router';

import { ButtonModule } from 'primeng/button';
import { MenuModule } from 'primeng/menu';

import { DotCollapseBreadcrumbComponent } from './dot-collapse-breadcrumb.component';
import { MAX_ITEMS } from './dot-collapse-breadcrumb.costants';

describe('DotCollapseBreadcrumbComponent', () => {
let spectator: Spectator<DotCollapseBreadcrumbComponent>;

const createComponent = createRoutingFactory({
component: DotCollapseBreadcrumbComponent,
providers: [ActivatedRoute],
imports: [MenuModule, ButtonModule]
});

beforeEach(() => {
spectator = createComponent();
});

it('should be created', () => {
spectator.detectChanges();
expect(spectator.component).toBeTruthy();
});

describe('MaxItems', () => {
it('should have the default', () => {
spectator.detectChanges();
expect(spectator.component.$maxItems()).toBe(MAX_ITEMS);
});

it('should show the options without btn collapse', () => {
spectator.setInput('maxItems', 5);
spectator.setInput('model', [
{ label: 'Electronics', url: '' },
{ label: 'Computer', url: '' },
{ label: 'Accessories', url: '' },
{ label: 'Keyboard', url: '' },
{ label: 'Wireless', url: '' }
]);
spectator.detectChanges();
expect(spectator.query(byTestId('btn-collapse'))).toBeNull();
expect(spectator.queryAll('.p-menuitem-link').length).toBe(5);
});

it('should show maxItems options with btn collapse', () => {
spectator.setInput('maxItems', 4);
spectator.setInput('model', [
{ label: 'Electronics', url: '' },
{ label: 'Computer', url: '' },
{ label: 'Accessories', url: '' },
{ label: 'Keyboard', url: '' },
{ label: 'Wireless', url: '' }
]);
spectator.detectChanges();
expect(spectator.query(byTestId('btn-collapse'))).toBeTruthy();
expect(spectator.queryAll('.p-menuitem-link').length).toBe(4);
});
});

it('should call itemClick when click home ', () => {
spectator.setInput('maxItems', 5);
spectator.setInput('model', [
{ label: 'Electronics', url: '' },
{ label: 'Computer', url: '' },
{ label: 'Accessories', url: '' },
{ label: 'Keyboard', url: '' },
{ label: 'Wireless', url: '' }
]);
spectator.detectChanges();

const itemClickSpy = spyOn(spectator.component.onItemClick, 'emit');
const firstEl = spectator.query('.p-menuitem-link');
spectator.click(firstEl);
spectator.detectChanges();

expect(itemClickSpy).toHaveBeenCalled();
});

it('should call itemClick when click home with routerLink', () => {
spectator.setInput('maxItems', 5);
spectator.setInput('model', [
{ label: 'Electronics', routerLink: '/' },
{ label: 'Computer', routerLink: '/' },
{ label: 'Accessories', routerLink: '/' },
{ label: 'Keyboard', routerLink: '/' },
{ label: 'Wireless', routerLink: '/' }
]);
spectator.detectChanges();

const itemClickSpy = spyOn(spectator.component.onItemClick, 'emit');
const firstEl = spectator.query('.p-menuitem-link');
spectator.click(firstEl);
spectator.detectChanges();

expect(itemClickSpy).toHaveBeenCalled();
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import { ChangeDetectionStrategy, Component, computed, input, output } from '@angular/core';
import { RouterModule } from '@angular/router';

import { MenuItem } from 'primeng/api';
import { BreadcrumbModule } from 'primeng/breadcrumb';
import { ButtonModule } from 'primeng/button';
import { ChevronRightIcon } from 'primeng/icons/chevronright';
import { MenuModule } from 'primeng/menu';

import { MAX_ITEMS } from './dot-collapse-breadcrumb.costants';
/**
* Component to display a breadcrumb with a collapse button
*
* @export
* @class DotCollapseBreadcrumbComponent
*/
@Component({
imports: [ChevronRightIcon, ButtonModule, MenuModule, RouterModule, BreadcrumbModule],
standalone: true,
selector: 'dot-collapse-breadcrumb',
templateUrl: './dot-collapse-breadcrumb.component.html',
changeDetection: ChangeDetectionStrategy.OnPush
})
export class DotCollapseBreadcrumbComponent {
/**
* Menu items to display
*
* @memberof DotCollapseBreadcrumbComponent
*/
$model = input<MenuItem[]>([], { alias: 'model' });
/**
* Max items to display
*
* @memberof DotCollapseBreadcrumbComponent
*/
$maxItems = input<number>(MAX_ITEMS, { alias: 'maxItems' });
/**
* Items to show
*
* @memberof DotCollapseBreadcrumbComponent
*/
$itemsToShow = computed(() => {
const items = this.$model();
const size = items.length;
const maxItems = this.$maxItems();

return size > maxItems ? items.slice(size - maxItems) : items;
});
/**
* Items to hide
*
* @memberof DotCollapseBreadcrumbComponent
*/
$itemsToHide = computed(() => {
const items = this.$model();
const size = items.length;
const maxItems = this.$maxItems();

return size > maxItems ? items.slice(0, size - maxItems) : [];
});
/**
* Indicates if the menu is collapsed
*
* @memberof DotCollapseBreadcrumbComponent
*/
$isCollapsed = computed(() => this.$itemsToHide().length > 0);
/**
* Event emitted when a menu item is clicked
*
* @memberof DotCollapseBreadcrumbComponent
*/
onItemClick = output<{ originalEvent: Event; item: MenuItem }>();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const MAX_ITEMS = 4;
Loading

0 comments on commit 4a03b35

Please sign in to comment.