From ec2e013ce6196c61763b64f2fa91231e44c8092f Mon Sep 17 00:00:00 2001 From: Olivia Guyot Date: Mon, 9 Dec 2024 17:52:45 +0100 Subject: [PATCH] feat(layout): move all pagination controls together, use common interface --- libs/ui/elements/src/index.ts | 2 - .../pagination-buttons.component.stories.ts | 41 ------- .../pagination/pagination.component.spec.ts | 68 ------------ .../pagination.component.stories.ts | 39 ------- .../lib/pagination/pagination.component.ts | 73 ------------ libs/ui/inputs/src/index.ts | 1 - ...previous-next-buttons.component.stories.ts | 37 ------ libs/ui/layout/src/index.ts | 4 + .../src/lib/carousel/carousel.component.css | 16 --- .../src/lib/carousel/carousel.component.html | 4 +- libs/ui/layout/src/lib/paginable.interface.ts | 14 +++ .../pagination-buttons.component.css | 0 .../pagination-buttons.component.html | 12 +- .../pagination-buttons.component.spec.ts | 34 +++--- .../pagination-buttons.component.stories.ts | 21 ++++ .../pagination-buttons.component.ts | 37 ++---- .../pagination-dots.component.css | 16 +++ .../pagination-dots.component.html | 15 +++ .../pagination-dots.component.spec.ts | 55 +++++++++ .../pagination-dots.component.stories.ts | 30 +++++ .../pagination-dots.component.ts | 31 ++++++ .../lib/pagination/pagination.component.css | 0 .../lib/pagination/pagination.component.html | 19 ++-- .../pagination/pagination.component.spec.ts | 105 ++++++++++++++++++ .../pagination.component.stories.ts | 102 +++++++++++++++++ .../lib/pagination/pagination.component.ts | 45 ++++++++ .../previous-next-buttons.component.css | 0 .../previous-next-buttons.component.html | 12 +- .../previous-next-buttons.component.spec.ts | 21 +++- ...previous-next-buttons.component.stories.ts | 26 +++++ .../previous-next-buttons.component.ts | 25 +---- 31 files changed, 539 insertions(+), 366 deletions(-) delete mode 100644 libs/ui/elements/src/lib/pagination-buttons/pagination-buttons.component.stories.ts delete mode 100644 libs/ui/elements/src/lib/pagination/pagination.component.spec.ts delete mode 100644 libs/ui/elements/src/lib/pagination/pagination.component.stories.ts delete mode 100644 libs/ui/elements/src/lib/pagination/pagination.component.ts delete mode 100644 libs/ui/inputs/src/lib/previous-next-buttons/previous-next-buttons.component.stories.ts create mode 100644 libs/ui/layout/src/lib/paginable.interface.ts rename libs/ui/{elements => layout}/src/lib/pagination-buttons/pagination-buttons.component.css (100%) rename libs/ui/{elements => layout}/src/lib/pagination-buttons/pagination-buttons.component.html (69%) rename libs/ui/{elements => layout}/src/lib/pagination-buttons/pagination-buttons.component.spec.ts (83%) create mode 100644 libs/ui/layout/src/lib/pagination-buttons/pagination-buttons.component.stories.ts rename libs/ui/{elements => layout}/src/lib/pagination-buttons/pagination-buttons.component.ts (66%) create mode 100644 libs/ui/layout/src/lib/pagination-dots/pagination-dots.component.css create mode 100644 libs/ui/layout/src/lib/pagination-dots/pagination-dots.component.html create mode 100644 libs/ui/layout/src/lib/pagination-dots/pagination-dots.component.spec.ts create mode 100644 libs/ui/layout/src/lib/pagination-dots/pagination-dots.component.stories.ts create mode 100644 libs/ui/layout/src/lib/pagination-dots/pagination-dots.component.ts rename libs/ui/{elements => layout}/src/lib/pagination/pagination.component.css (100%) rename libs/ui/{elements => layout}/src/lib/pagination/pagination.component.html (72%) create mode 100644 libs/ui/layout/src/lib/pagination/pagination.component.spec.ts create mode 100644 libs/ui/layout/src/lib/pagination/pagination.component.stories.ts create mode 100644 libs/ui/layout/src/lib/pagination/pagination.component.ts rename libs/ui/{inputs => layout}/src/lib/previous-next-buttons/previous-next-buttons.component.css (100%) rename libs/ui/{inputs => layout}/src/lib/previous-next-buttons/previous-next-buttons.component.html (55%) rename libs/ui/{inputs => layout}/src/lib/previous-next-buttons/previous-next-buttons.component.spec.ts (80%) create mode 100644 libs/ui/layout/src/lib/previous-next-buttons/previous-next-buttons.component.stories.ts rename libs/ui/{inputs => layout}/src/lib/previous-next-buttons/previous-next-buttons.component.ts (53%) diff --git a/libs/ui/elements/src/index.ts b/libs/ui/elements/src/index.ts index 1e62e06968..7d8c2c6537 100644 --- a/libs/ui/elements/src/index.ts +++ b/libs/ui/elements/src/index.ts @@ -15,8 +15,6 @@ export * from './lib/metadata-info/metadata-info.component' export * from './lib/metadata-quality-item/metadata-quality-item.component' export * from './lib/metadata-quality/metadata-quality.component' export * from './lib/notification/notification.component' -export * from './lib/pagination-buttons/pagination-buttons.component' -export * from './lib/pagination/pagination.component' export * from './lib/record-api-form/record-api-form.component' export * from './lib/related-record-card/related-record-card.component' export * from './lib/thumbnail/thumbnail.component' diff --git a/libs/ui/elements/src/lib/pagination-buttons/pagination-buttons.component.stories.ts b/libs/ui/elements/src/lib/pagination-buttons/pagination-buttons.component.stories.ts deleted file mode 100644 index 70cc9ecee5..0000000000 --- a/libs/ui/elements/src/lib/pagination-buttons/pagination-buttons.component.stories.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { TranslateModule } from '@ngx-translate/core' -import { Meta, moduleMetadata, StoryObj } from '@storybook/angular' -import { - TRANSLATE_DEFAULT_CONFIG, - UtilI18nModule, -} from '@geonetwork-ui/util/i18n' -import { ButtonComponent } from '@geonetwork-ui/ui/inputs' -import { PaginationButtonsComponent } from './pagination-buttons.component' -import { FormsModule } from '@angular/forms' -import { action } from '@storybook/addon-actions' - -export default { - title: 'Elements/PaginationButtonsComponent', - component: PaginationButtonsComponent, - decorators: [ - moduleMetadata({ - imports: [ - ButtonComponent, - UtilI18nModule, - FormsModule, - TranslateModule.forRoot(TRANSLATE_DEFAULT_CONFIG), - ], - }), - ], - render: (args: PaginationButtonsComponent) => ({ - props: { - ...args, - newCurrentPageEvent: action('newCurrentPageEvent'), - }, - }), -} as Meta - -export const Primary: StoryObj = { - args: { - currentPage: 1, - totalPages: 10, - }, - parameters: { - layout: 'centered', - }, -} diff --git a/libs/ui/elements/src/lib/pagination/pagination.component.spec.ts b/libs/ui/elements/src/lib/pagination/pagination.component.spec.ts deleted file mode 100644 index 8d81779b3d..0000000000 --- a/libs/ui/elements/src/lib/pagination/pagination.component.spec.ts +++ /dev/null @@ -1,68 +0,0 @@ -import { ChangeDetectionStrategy } from '@angular/core' -import { ComponentFixture, TestBed } from '@angular/core/testing' -import { By } from '@angular/platform-browser' -import { PaginationComponent } from './pagination.component' -import { TranslateModule } from '@ngx-translate/core' -import { ButtonComponent } from '@geonetwork-ui/ui/inputs' - -describe('PaginationComponent', () => { - let component: PaginationComponent - let fixture: ComponentFixture - - beforeEach(async () => { - await TestBed.configureTestingModule({ - imports: [PaginationComponent, TranslateModule.forRoot()], - }) - .overrideComponent(PaginationComponent, { - set: { changeDetection: ChangeDetectionStrategy.Default }, - }) - .compileComponents() - - fixture = TestBed.createComponent(PaginationComponent) - component = fixture.componentInstance - component.currentPage = 10 - component.nPages = 10 - component.hideButton = false - fixture.detectChanges() - }) - - it('should create', () => { - expect(component).toBeTruthy() - }) - - describe('next button', () => { - let btn - describe('by default', () => { - beforeEach(() => { - btn = fixture.debugElement.query(By.css('gn-ui-button[type=secondary]')) - }) - it('is displayed', () => { - expect(btn).toBeTruthy() - }) - }) - describe('if hidden', () => { - beforeEach(() => { - component.hideButton = true - fixture.detectChanges() - btn = fixture.debugElement.query(By.css('gn-ui-button[type=secondary]')) - }) - it('is displayed', () => { - expect(btn).toBeFalsy() - }) - }) - }) - - it('should navigate_next be disabled', () => { - const isDisabled = fixture.debugElement.queryAll( - By.directive(ButtonComponent) - )[0].componentInstance.disabled - expect(isDisabled).toBeTruthy() - }) - - it('should navigate_previous be enabled', () => { - const isDisabled = fixture.debugElement.queryAll( - By.directive(ButtonComponent) - )[1].componentInstance.disabled - expect(isDisabled).toBeFalsy() - }) -}) diff --git a/libs/ui/elements/src/lib/pagination/pagination.component.stories.ts b/libs/ui/elements/src/lib/pagination/pagination.component.stories.ts deleted file mode 100644 index 2879c62f0e..0000000000 --- a/libs/ui/elements/src/lib/pagination/pagination.component.stories.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { TranslateModule } from '@ngx-translate/core' -import { - componentWrapperDecorator, - Meta, - moduleMetadata, - StoryObj, -} from '@storybook/angular' -import { - TRANSLATE_DEFAULT_CONFIG, - UtilI18nModule, -} from '@geonetwork-ui/util/i18n' -import { PaginationComponent } from './pagination.component' -import { ButtonComponent } from '@geonetwork-ui/ui/inputs' -import { FormsModule } from '@angular/forms' - -export default { - title: 'Elements/PaginationComponent', - component: PaginationComponent, - decorators: [ - moduleMetadata({ - imports: [ - ButtonComponent, - UtilI18nModule, - FormsModule, - TranslateModule.forRoot(TRANSLATE_DEFAULT_CONFIG), - ], - }), - componentWrapperDecorator( - (story) => `
${story}
` - ), - ], -} as Meta - -export const Primary: StoryObj = { - args: { - currentPage: 1, - nPages: 10, - }, -} diff --git a/libs/ui/elements/src/lib/pagination/pagination.component.ts b/libs/ui/elements/src/lib/pagination/pagination.component.ts deleted file mode 100644 index 67f5bd7546..0000000000 --- a/libs/ui/elements/src/lib/pagination/pagination.component.ts +++ /dev/null @@ -1,73 +0,0 @@ -import { - ChangeDetectionStrategy, - Component, - EventEmitter, - Input, - OnChanges, - Output, - SimpleChanges, -} from '@angular/core' -import { ButtonComponent } from '@geonetwork-ui/ui/inputs' -import { NgIcon, provideIcons } from '@ng-icons/core' -import { FormsModule } from '@angular/forms' -import { - matChevronLeft, - matChevronRight, -} from '@ng-icons/material-icons/baseline' -import { CommonModule } from '@angular/common' -import { TranslateModule } from '@ngx-translate/core' - -@Component({ - selector: 'gn-ui-pagination', - templateUrl: './pagination.component.html', - styleUrls: ['./pagination.component.css'], - changeDetection: ChangeDetectionStrategy.OnPush, - standalone: true, - imports: [ - CommonModule, - ButtonComponent, - NgIcon, - FormsModule, - TranslateModule, - ], - viewProviders: [ - provideIcons({ - matChevronLeft, - matChevronRight, - }), - ], -}) -export class PaginationComponent implements OnChanges { - @Input() currentPage = 1 - @Input() nPages = 1 - @Input() hideButton = false - @Output() newCurrentPageEvent = new EventEmitter() - - private applyPageBounds() { - // make sure this works with NaN inputs as well by adding `|| 1` - this.nPages = Math.max(1, this.nPages || 1) - this.currentPage = Math.max(1, Math.min(this.nPages, this.currentPage || 1)) - } - - ngOnChanges(changes: SimpleChanges) { - // make sure the inputs are valid - if ('currentPage' in changes || 'nPages' in changes) { - this.applyPageBounds() - } - } - - setPage(newPage) { - if (!Number.isInteger(newPage)) return - this.currentPage = newPage - this.applyPageBounds() - this.newCurrentPageEvent.emit(this.currentPage) - } - - nextPage() { - this.setPage(this.currentPage + 1) - } - - previousPage() { - this.setPage(this.currentPage - 1) - } -} diff --git a/libs/ui/inputs/src/index.ts b/libs/ui/inputs/src/index.ts index e87fa440fe..c55f95dca2 100644 --- a/libs/ui/inputs/src/index.ts +++ b/libs/ui/inputs/src/index.ts @@ -22,7 +22,6 @@ export * from './lib/text-input/text-input.component' export * from './lib/ui-inputs.module' export * from './lib/url-input/url-input.component' export * from './lib/viewport-intersector/viewport-intersector.component' -export * from './lib/previous-next-buttons/previous-next-buttons.component' export * from './lib/switch-toggle/switch-toggle.component' export * from './lib/file-input/file-input.component' export * from './lib/image-input/image-input.component' diff --git a/libs/ui/inputs/src/lib/previous-next-buttons/previous-next-buttons.component.stories.ts b/libs/ui/inputs/src/lib/previous-next-buttons/previous-next-buttons.component.stories.ts deleted file mode 100644 index 47d8a272ba..0000000000 --- a/libs/ui/inputs/src/lib/previous-next-buttons/previous-next-buttons.component.stories.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { Meta, moduleMetadata, StoryObj } from '@storybook/angular' -import { PreviousNextButtonsComponent } from './previous-next-buttons.component' -import { TranslateModule } from '@ngx-translate/core' -import { - TRANSLATE_DEFAULT_CONFIG, - UtilI18nModule, -} from '@geonetwork-ui/util/i18n' - -export default { - title: 'Inputs/PreviousNextButtonsComponent', - component: PreviousNextButtonsComponent, - parameters: { - backgrounds: { - default: 'dark', - }, - }, - decorators: [ - moduleMetadata({ - imports: [ - UtilI18nModule, - TranslateModule.forRoot(TRANSLATE_DEFAULT_CONFIG), - ], - }), - ], -} as Meta - -export const Primary: StoryObj = { - args: { - isFirst: true, - isLast: false, - }, - render: (args) => ({ - props: args, - template: - '', - }), -} diff --git a/libs/ui/layout/src/index.ts b/libs/ui/layout/src/index.ts index 00930a80ee..8233c4e3f1 100644 --- a/libs/ui/layout/src/index.ts +++ b/libs/ui/layout/src/index.ts @@ -10,4 +10,8 @@ export * from './lib/sticky-header/sticky-header.component' export * from './lib/block-list/block-list.component' export * from './lib/sortable-list/sortable-list.component' export * from './lib/modal-dialog/modal-dialog.component' +export * from './lib/pagination/pagination.component' +export * from './lib/pagination-buttons/pagination-buttons.component' +export * from './lib/previous-next-buttons/previous-next-buttons.component' +export * from './lib/paginable.interface' export * from './lib/ui-layout.module' diff --git a/libs/ui/layout/src/lib/carousel/carousel.component.css b/libs/ui/layout/src/lib/carousel/carousel.component.css index 33364704bc..09abc47b36 100644 --- a/libs/ui/layout/src/lib/carousel/carousel.component.css +++ b/libs/ui/layout/src/lib/carousel/carousel.component.css @@ -6,19 +6,3 @@ position: relative; display: block; } - -.carousel-step-dot { - width: 6px; - height: 6px; - border-radius: 6px; - position: relative; -} - -.carousel-step-dot:after { - content: ''; - position: absolute; - left: -7px; - top: -7px; - width: 20px; - height: 20px; -} diff --git a/libs/ui/layout/src/lib/carousel/carousel.component.html b/libs/ui/layout/src/lib/carousel/carousel.component.html index 0c2ae552d9..bc78c8c816 100644 --- a/libs/ui/layout/src/lib/carousel/carousel.component.html +++ b/libs/ui/layout/src/lib/carousel/carousel.component.html @@ -10,8 +10,8 @@ > diff --git a/libs/ui/layout/src/lib/paginable.interface.ts b/libs/ui/layout/src/lib/paginable.interface.ts new file mode 100644 index 0000000000..a696dbfa40 --- /dev/null +++ b/libs/ui/layout/src/lib/paginable.interface.ts @@ -0,0 +1,14 @@ +/** + * This interface is used for components that want to offer pagination + * Note: pages indexes are 1-based!! so `isLastPage` means `currentPage === pagesCount` + * and `isFirstPage` means `currentPage === 1` + */ +export interface Paginable { + isFirstPage: boolean + isLastPage: boolean + pagesCount: number + currentPage: number + goToPage(index: number): void + goToNextPage(): void + goToPrevPage(): void +} diff --git a/libs/ui/elements/src/lib/pagination-buttons/pagination-buttons.component.css b/libs/ui/layout/src/lib/pagination-buttons/pagination-buttons.component.css similarity index 100% rename from libs/ui/elements/src/lib/pagination-buttons/pagination-buttons.component.css rename to libs/ui/layout/src/lib/pagination-buttons/pagination-buttons.component.css diff --git a/libs/ui/elements/src/lib/pagination-buttons/pagination-buttons.component.html b/libs/ui/layout/src/lib/pagination-buttons/pagination-buttons.component.html similarity index 69% rename from libs/ui/elements/src/lib/pagination-buttons/pagination-buttons.component.html rename to libs/ui/layout/src/lib/pagination-buttons/pagination-buttons.component.html index a6dbd5bac5..115bcf154a 100644 --- a/libs/ui/elements/src/lib/pagination-buttons/pagination-buttons.component.html +++ b/libs/ui/layout/src/lib/pagination-buttons/pagination-buttons.component.html @@ -2,8 +2,8 @@
@@ -14,8 +14,8 @@ {{ page }} @@ -23,8 +23,8 @@ diff --git a/libs/ui/elements/src/lib/pagination-buttons/pagination-buttons.component.spec.ts b/libs/ui/layout/src/lib/pagination-buttons/pagination-buttons.component.spec.ts similarity index 83% rename from libs/ui/elements/src/lib/pagination-buttons/pagination-buttons.component.spec.ts rename to libs/ui/layout/src/lib/pagination-buttons/pagination-buttons.component.spec.ts index 71735ad743..9d84597da5 100644 --- a/libs/ui/elements/src/lib/pagination-buttons/pagination-buttons.component.spec.ts +++ b/libs/ui/layout/src/lib/pagination-buttons/pagination-buttons.component.spec.ts @@ -1,14 +1,21 @@ import { ComponentFixture, TestBed } from '@angular/core/testing' import { PaginationButtonsComponent } from './pagination-buttons.component' +import { Paginable } from '../paginable.interface' + +class MockPaginable implements Paginable { + currentPage = 1 + pagesCount = 5 + isFirstPage = true + isLastPage = false + goToPage = jest.fn() + goToPrevPage = jest.fn() + goToNextPage = jest.fn() +} describe('PaginationButtonsComponent', () => { let component: PaginationButtonsComponent let fixture: ComponentFixture - const mockChangePage = (page) => { - component.setPage(page) - } - beforeEach(async () => { await TestBed.configureTestingModule({ imports: [PaginationButtonsComponent], @@ -16,19 +23,17 @@ describe('PaginationButtonsComponent', () => { fixture = TestBed.createComponent(PaginationButtonsComponent) component = fixture.componentInstance - component.currentPage = 3 - component.totalPages = 10 + component.listComponent = new MockPaginable() component.calculateVisiblePages() - component.changePage = mockChangePage fixture.detectChanges() }) it('should create', () => { expect(component).toBeTruthy() }) + describe('when using next arrow', () => { beforeEach(() => { - component.currentPage = 2 const paginationButtons = fixture.nativeElement.querySelectorAll('gn-ui-button') paginationButtons.forEach((buttonElement) => { @@ -39,12 +44,11 @@ describe('PaginationButtonsComponent', () => { }) }) it('should access next page on click', () => { - expect(component.currentPage).toBe(3) + expect(component.listComponent.goToNextPage).toHaveBeenCalled() }) }) describe('when using previous arrow', () => { beforeEach(() => { - component.currentPage = 4 const paginationButtons = fixture.nativeElement.querySelectorAll('gn-ui-button') paginationButtons.forEach((buttonElement) => { @@ -54,13 +58,13 @@ describe('PaginationButtonsComponent', () => { } }) }) - it('is should access previous page', () => { - expect(component.currentPage).toBe(3) + it('should access previous page on click', () => { + expect(component.listComponent.goToPrevPage).toHaveBeenCalled() }) }) describe('when accessing first page', () => { beforeEach(() => { - component.currentPage = 1 + component.listComponent.isFirstPage = true fixture.detectChanges() }) it('is should disable the previous arrow', () => { @@ -77,7 +81,7 @@ describe('PaginationButtonsComponent', () => { }) describe('when accessing last page', () => { beforeEach(() => { - component.currentPage = 10 + component.listComponent.isLastPage = true fixture.detectChanges() }) it('is should disable the next arrow', () => { @@ -106,7 +110,7 @@ describe('PaginationButtonsComponent', () => { pageBtnList[1].dispatchEvent(new Event('buttonClick')) }) it('is should access the requested page', () => { - expect(component.currentPage).toBe(2) + expect(component.listComponent.goToPage).toHaveBeenCalledWith(2) }) }) }) diff --git a/libs/ui/layout/src/lib/pagination-buttons/pagination-buttons.component.stories.ts b/libs/ui/layout/src/lib/pagination-buttons/pagination-buttons.component.stories.ts new file mode 100644 index 0000000000..b44214027c --- /dev/null +++ b/libs/ui/layout/src/lib/pagination-buttons/pagination-buttons.component.stories.ts @@ -0,0 +1,21 @@ +import { Meta, moduleMetadata, StoryObj } from '@storybook/angular' +import { PaginationButtonsComponent } from './pagination-buttons.component' +import { MockListComponent } from '../pagination/pagination.component.stories' + +export default { + title: 'Layout/Pagination/PaginationButtonsComponent', + component: PaginationButtonsComponent, + decorators: [ + moduleMetadata({ + imports: [MockListComponent], + }), + ], +} as Meta + +export const Primary: StoryObj = { + render: () => ({ + template: ` + +`, + }), +} diff --git a/libs/ui/elements/src/lib/pagination-buttons/pagination-buttons.component.ts b/libs/ui/layout/src/lib/pagination-buttons/pagination-buttons.component.ts similarity index 66% rename from libs/ui/elements/src/lib/pagination-buttons/pagination-buttons.component.ts rename to libs/ui/layout/src/lib/pagination-buttons/pagination-buttons.component.ts index b69928c602..6cfc681b7e 100644 --- a/libs/ui/elements/src/lib/pagination-buttons/pagination-buttons.component.ts +++ b/libs/ui/layout/src/lib/pagination-buttons/pagination-buttons.component.ts @@ -1,14 +1,9 @@ -import { - Component, - EventEmitter, - Input, - OnChanges, - Output, -} from '@angular/core' +import { Component, Input, OnChanges } from '@angular/core' import { ButtonComponent } from '@geonetwork-ui/ui/inputs' import { NgIcon, provideIcons } from '@ng-icons/core' import { CommonModule } from '@angular/common' import { iconoirNavArrowLeft, iconoirNavArrowRight } from '@ng-icons/iconoir' +import { Paginable } from '../paginable.interface' @Component({ selector: 'gn-ui-pagination-buttons', @@ -24,10 +19,8 @@ import { iconoirNavArrowLeft, iconoirNavArrowRight } from '@ng-icons/iconoir' ], }) export class PaginationButtonsComponent implements OnChanges { - @Input() currentPage: number - @Input() totalPages: number + @Input() listComponent: Paginable visiblePages: (number | '...')[] = [] - @Output() newCurrentPageEvent = new EventEmitter() ngOnChanges(): void { this.calculateVisiblePages() @@ -36,8 +29,11 @@ export class PaginationButtonsComponent implements OnChanges { calculateVisiblePages(): void { const maxVisiblePages = 5 const halfVisible = Math.floor(maxVisiblePages / 2) - const startPage = Math.max(this.currentPage - halfVisible, 1) - const endPage = Math.min(this.currentPage + halfVisible, this.totalPages) + const startPage = Math.max(this.listComponent.currentPage - halfVisible, 1) + const endPage = Math.min( + this.listComponent.currentPage + halfVisible, + this.listComponent.pagesCount + ) const visiblePages: (number | '...')[] = [] if (startPage > 1) { @@ -49,11 +45,11 @@ export class PaginationButtonsComponent implements OnChanges { for (let page = startPage; page <= endPage; page++) { visiblePages.push(page) } - if (endPage < this.totalPages) { - if (endPage < this.totalPages - 1) { + if (endPage < this.listComponent.pagesCount) { + if (endPage < this.listComponent.pagesCount - 1) { visiblePages.push('...') } - visiblePages.push(this.totalPages) + visiblePages.push(this.listComponent.pagesCount) } this.visiblePages = visiblePages @@ -63,18 +59,9 @@ export class PaginationButtonsComponent implements OnChanges { this.setPage(page) } - nextPage() { - this.setPage(this.currentPage + 1) - } - - previousPage() { - this.setPage(this.currentPage - 1) - } - setPage(newPage) { if (!Number.isInteger(newPage)) return - this.currentPage = newPage + this.listComponent.goToPage(newPage) this.calculateVisiblePages() - this.newCurrentPageEvent.emit(this.currentPage) } } diff --git a/libs/ui/layout/src/lib/pagination-dots/pagination-dots.component.css b/libs/ui/layout/src/lib/pagination-dots/pagination-dots.component.css new file mode 100644 index 0000000000..7263eb6669 --- /dev/null +++ b/libs/ui/layout/src/lib/pagination-dots/pagination-dots.component.css @@ -0,0 +1,16 @@ +.pagination-dot { + width: 6px; + height: 6px; + border-radius: 6px; + position: relative; + flex-shrink: 0; +} + +.pagination-dot:after { + content: ''; + position: absolute; + left: -7px; + top: -7px; + width: 20px; + height: 20px; +} diff --git a/libs/ui/layout/src/lib/pagination-dots/pagination-dots.component.html b/libs/ui/layout/src/lib/pagination-dots/pagination-dots.component.html new file mode 100644 index 0000000000..923c8093f0 --- /dev/null +++ b/libs/ui/layout/src/lib/pagination-dots/pagination-dots.component.html @@ -0,0 +1,15 @@ +
+ +
diff --git a/libs/ui/layout/src/lib/pagination-dots/pagination-dots.component.spec.ts b/libs/ui/layout/src/lib/pagination-dots/pagination-dots.component.spec.ts new file mode 100644 index 0000000000..1a4795d31f --- /dev/null +++ b/libs/ui/layout/src/lib/pagination-dots/pagination-dots.component.spec.ts @@ -0,0 +1,55 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing' +import { PaginationDotsComponent } from './pagination-dots.component' +import { By } from '@angular/platform-browser' +import { Paginable } from '../paginable.interface' + +class MockPaginable implements Paginable { + currentPage = 4 + pagesCount = 5 + isFirstPage = false + isLastPage = false + goToPage = jest.fn() + goToPrevPage = jest.fn() + goToNextPage = jest.fn() +} + +describe('PaginationDotsComponent', () => { + let component: PaginationDotsComponent + let fixture: ComponentFixture + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [PaginationDotsComponent], + }).compileComponents() + + fixture = TestBed.createComponent(PaginationDotsComponent) + component = fixture.componentInstance + component.listComponent = new MockPaginable() + fixture.detectChanges() + }) + + it('should create', () => { + expect(component).toBeTruthy() + }) + + describe('dots', () => { + let dots: HTMLElement[] + beforeEach(() => { + dots = fixture.debugElement + .queryAll(By.css('.pagination-dot')) + .map((dot) => dot.nativeElement) + }) + it('has 1 dot per page', () => { + expect(dots.length).toBe(component.listComponent.pagesCount) + }) + it('switches to a page on click', () => { + dots[2].click() + expect(component.listComponent.goToPage).toHaveBeenCalledWith(3) // page index is 1-based + }) + it('shows selected page as active', () => { + expect(dots[2].classList).not.toContain('bg-primary') + expect(dots[3].classList).toContain('bg-primary') + expect(dots[4].classList).not.toContain('bg-primary') + }) + }) +}) diff --git a/libs/ui/layout/src/lib/pagination-dots/pagination-dots.component.stories.ts b/libs/ui/layout/src/lib/pagination-dots/pagination-dots.component.stories.ts new file mode 100644 index 0000000000..cecd216eee --- /dev/null +++ b/libs/ui/layout/src/lib/pagination-dots/pagination-dots.component.stories.ts @@ -0,0 +1,30 @@ +import { + componentWrapperDecorator, + Meta, + moduleMetadata, + StoryObj, +} from '@storybook/angular' +import { PaginationDotsComponent } from './pagination-dots.component' +import { MockListComponent } from '../pagination/pagination.component.stories' + +export default { + title: 'Layout/Pagination/PaginationDotsComponent', + component: PaginationDotsComponent, + decorators: [ + moduleMetadata({ + imports: [MockListComponent], + }), + componentWrapperDecorator( + (story) => + `
${story}
` + ), + ], +} as Meta + +export const Primary: StoryObj = { + render: () => ({ + template: ` + +`, + }), +} diff --git a/libs/ui/layout/src/lib/pagination-dots/pagination-dots.component.ts b/libs/ui/layout/src/lib/pagination-dots/pagination-dots.component.ts new file mode 100644 index 0000000000..e989b0dc07 --- /dev/null +++ b/libs/ui/layout/src/lib/pagination-dots/pagination-dots.component.ts @@ -0,0 +1,31 @@ +import { Component, Input } from '@angular/core' +import { provideIcons } from '@ng-icons/core' +import { CommonModule } from '@angular/common' +import { iconoirNavArrowLeft, iconoirNavArrowRight } from '@ng-icons/iconoir' +import { Paginable } from '../paginable.interface' + +@Component({ + selector: 'gn-ui-pagination-dots', + templateUrl: './pagination-dots.component.html', + styleUrls: ['./pagination-dots.component.css'], + standalone: true, + imports: [CommonModule], + viewProviders: [ + provideIcons({ + iconoirNavArrowRight, + iconoirNavArrowLeft, + }), + ], +}) +export class PaginationDotsComponent { + @Input() listComponent: Paginable + @Input() containerClass = '' + + // 1-based + get steps() { + return Array.from( + { length: this.listComponent.pagesCount }, + (_, i) => i + 1 + ) + } +} diff --git a/libs/ui/elements/src/lib/pagination/pagination.component.css b/libs/ui/layout/src/lib/pagination/pagination.component.css similarity index 100% rename from libs/ui/elements/src/lib/pagination/pagination.component.css rename to libs/ui/layout/src/lib/pagination/pagination.component.css diff --git a/libs/ui/elements/src/lib/pagination/pagination.component.html b/libs/ui/layout/src/lib/pagination/pagination.component.html similarity index 72% rename from libs/ui/elements/src/lib/pagination/pagination.component.html rename to libs/ui/layout/src/lib/pagination/pagination.component.html index 9c9ac2a487..99d55d0a1e 100644 --- a/libs/ui/elements/src/lib/pagination/pagination.component.html +++ b/libs/ui/layout/src/lib/pagination/pagination.component.html @@ -1,9 +1,9 @@
pagination.pageOf {{ nPages }}pagination.pageOf + {{ listComponent.pagesCount }} { + let component: PaginationComponent + let fixture: ComponentFixture + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [PaginationComponent, TranslateModule.forRoot()], + }).compileComponents() + + fixture = TestBed.createComponent(PaginationComponent) + component = fixture.componentInstance + component.listComponent = new MockPaginable() + component.hideButton = false + fixture.detectChanges() + }) + + it('should create', () => { + expect(component).toBeTruthy() + }) + + describe('next button', () => { + let btn: ButtonComponent + beforeEach(() => { + btn = fixture.debugElement.queryAll(By.directive(ButtonComponent))[0] + ?.componentInstance + }) + it('is displayed by default', () => { + expect(btn).toBeTruthy() + }) + it('is hidden if hideButton = true', () => { + component.hideButton = true + fixture.detectChanges() + expect( + fixture.debugElement.queryAll(By.directive(ButtonComponent)).length + ).toBe(2) + }) + it('is disabled if last page', () => { + component.listComponent.isLastPage = true + fixture.detectChanges() + expect(btn.disabled).toBe(true) + }) + it('goes to next page', () => { + btn.buttonClick.emit() + expect(component.listComponent.goToNextPage).toHaveBeenCalled() + }) + }) + + describe('prev and next buttons', () => { + let prevButton: ButtonComponent + let nextButton: ButtonComponent + beforeEach(() => { + prevButton = fixture.debugElement.queryAll( + By.directive(ButtonComponent) + )[1].componentInstance + nextButton = fixture.debugElement.queryAll( + By.directive(ButtonComponent) + )[2].componentInstance + }) + it('prev button disabled if first page', () => { + component.listComponent.isFirstPage = true + fixture.detectChanges() + expect(prevButton.disabled).toBe(true) + }) + it('prev button enabled if not first page', () => { + component.listComponent.isFirstPage = false + fixture.detectChanges() + expect(prevButton.disabled).toBe(false) + }) + it('calls goToPrevPage', () => { + prevButton.buttonClick.emit() + expect(component.listComponent.goToPrevPage).toHaveBeenCalled() + }) + it('next button disabled if last page', () => { + component.listComponent.isLastPage = true + fixture.detectChanges() + expect(nextButton.disabled).toBe(true) + }) + it('next button enabled if not last page', () => { + component.listComponent.isLastPage = false + fixture.detectChanges() + expect(nextButton.disabled).toBe(false) + }) + it('calls goToNextPage', () => { + nextButton.buttonClick.emit() + expect(component.listComponent.goToNextPage).toHaveBeenCalled() + }) + }) +}) diff --git a/libs/ui/layout/src/lib/pagination/pagination.component.stories.ts b/libs/ui/layout/src/lib/pagination/pagination.component.stories.ts new file mode 100644 index 0000000000..7f60821d94 --- /dev/null +++ b/libs/ui/layout/src/lib/pagination/pagination.component.stories.ts @@ -0,0 +1,102 @@ +import { + applicationConfig, + componentWrapperDecorator, + Meta, + moduleMetadata, + StoryObj, +} from '@storybook/angular' +import { PaginationComponent } from './pagination.component' +import { + ChangeDetectionStrategy, + ChangeDetectorRef, + Component, + importProvidersFrom, +} from '@angular/core' +import { Paginable } from '../paginable.interface' +import { + TRANSLATE_DEFAULT_CONFIG, + UtilI18nModule, +} from '@geonetwork-ui/util/i18n' +import { TranslateModule } from '@ngx-translate/core' + +@Component({ + selector: 'gn-ui-mock-list', + template: `current page: {{ currentPage }}
+   +
+
pages count: {{ pagesCount }}
`, + changeDetection: ChangeDetectionStrategy.OnPush, + standalone: true, +}) +export class MockListComponent implements Paginable { + currentPage = 1 + pagesCount = 8 + constructor(private changeDetector: ChangeDetectorRef) {} + get isFirstPage() { + return this.currentPage == 1 + } + get isLastPage() { + return this.currentPage == this.pagesCount + } + goToPage(index: number) { + this.currentPage = index + this.changeDetector.detectChanges() + } + goToPrevPage() { + if (this.isFirstPage) return + this.goToPage(this.currentPage - 1) + } + goToNextPage() { + if (this.isLastPage) return + this.goToPage(this.currentPage + 1) + } +} + +export default { + title: 'Layout/Pagination/PaginationComponent', + component: PaginationComponent, + decorators: [ + moduleMetadata({ + imports: [MockListComponent], + }), + applicationConfig({ + providers: [ + importProvidersFrom(UtilI18nModule), + importProvidersFrom(TranslateModule.forRoot(TRANSLATE_DEFAULT_CONFIG)), + ], + }), + componentWrapperDecorator( + (story) => + `
${story}
` + ), + ], +} as Meta + +export const Primary: StoryObj = { + args: { + hideButton: false, + }, + argTypes: { + hideButton: { + control: 'boolean', + }, + }, + render: (args) => ({ + props: args, + template: ` + +`, + }), +} diff --git a/libs/ui/layout/src/lib/pagination/pagination.component.ts b/libs/ui/layout/src/lib/pagination/pagination.component.ts new file mode 100644 index 0000000000..62bebe78d9 --- /dev/null +++ b/libs/ui/layout/src/lib/pagination/pagination.component.ts @@ -0,0 +1,45 @@ +import { Component, Input } from '@angular/core' +import { ButtonComponent } from '@geonetwork-ui/ui/inputs' +import { NgIcon, provideIcons } from '@ng-icons/core' +import { FormsModule } from '@angular/forms' +import { + matChevronLeft, + matChevronRight, +} from '@ng-icons/material-icons/baseline' +import { CommonModule } from '@angular/common' +import { TranslateModule } from '@ngx-translate/core' +import { Paginable } from '../paginable.interface' + +@Component({ + selector: 'gn-ui-pagination', + templateUrl: './pagination.component.html', + styleUrls: ['./pagination.component.css'], + standalone: true, + imports: [ + CommonModule, + ButtonComponent, + NgIcon, + FormsModule, + TranslateModule, + ], + viewProviders: [ + provideIcons({ + matChevronLeft, + matChevronRight, + }), + ], +}) +export class PaginationComponent { + @Input() listComponent: Paginable + @Input() hideButton = false + + private applyPageBounds(page: number): number { + // make sure this works with NaN inputs as well by adding `|| 1` + return Math.max(1, Math.min(this.listComponent.pagesCount, page || 1)) + } + + setPage(newPage) { + if (!Number.isInteger(newPage)) return + this.listComponent.goToPage(this.applyPageBounds(newPage)) + } +} diff --git a/libs/ui/inputs/src/lib/previous-next-buttons/previous-next-buttons.component.css b/libs/ui/layout/src/lib/previous-next-buttons/previous-next-buttons.component.css similarity index 100% rename from libs/ui/inputs/src/lib/previous-next-buttons/previous-next-buttons.component.css rename to libs/ui/layout/src/lib/previous-next-buttons/previous-next-buttons.component.css diff --git a/libs/ui/inputs/src/lib/previous-next-buttons/previous-next-buttons.component.html b/libs/ui/layout/src/lib/previous-next-buttons/previous-next-buttons.component.html similarity index 55% rename from libs/ui/inputs/src/lib/previous-next-buttons/previous-next-buttons.component.html rename to libs/ui/layout/src/lib/previous-next-buttons/previous-next-buttons.component.html index ed38ca89ca..4ce91cf319 100644 --- a/libs/ui/inputs/src/lib/previous-next-buttons/previous-next-buttons.component.html +++ b/libs/ui/layout/src/lib/previous-next-buttons/previous-next-buttons.component.html @@ -1,17 +1,17 @@
diff --git a/libs/ui/inputs/src/lib/previous-next-buttons/previous-next-buttons.component.spec.ts b/libs/ui/layout/src/lib/previous-next-buttons/previous-next-buttons.component.spec.ts similarity index 80% rename from libs/ui/inputs/src/lib/previous-next-buttons/previous-next-buttons.component.spec.ts rename to libs/ui/layout/src/lib/previous-next-buttons/previous-next-buttons.component.spec.ts index 95ac4c3eb5..588e4b5264 100644 --- a/libs/ui/inputs/src/lib/previous-next-buttons/previous-next-buttons.component.spec.ts +++ b/libs/ui/layout/src/lib/previous-next-buttons/previous-next-buttons.component.spec.ts @@ -1,9 +1,19 @@ import { ComponentFixture, TestBed } from '@angular/core/testing' - import { PreviousNextButtonsComponent } from './previous-next-buttons.component' import { TranslateModule } from '@ngx-translate/core' import { By } from '@angular/platform-browser' import { DebugElement } from '@angular/core' +import { Paginable } from '../paginable.interface' + +class MockPaginable implements Paginable { + currentPage = 1 + pagesCount = 5 + isFirstPage = true + isLastPage = false + goToPage = jest.fn() + goToPrevPage = jest.fn() + goToNextPage = jest.fn() +} describe('PreviousNextButtonsComponent', () => { let component: PreviousNextButtonsComponent @@ -19,6 +29,7 @@ describe('PreviousNextButtonsComponent', () => { beforeEach(() => { fixture = TestBed.createComponent(PreviousNextButtonsComponent) component = fixture.componentInstance + component.listComponent = new MockPaginable() compiled = fixture.debugElement }) @@ -28,8 +39,8 @@ describe('PreviousNextButtonsComponent', () => { describe('onFirstElement', () => { beforeEach(() => { - component.isFirst = true - component.isLast = false + component.listComponent.isFirstPage = true + component.listComponent.isLastPage = false fixture.detectChanges() }) @@ -48,8 +59,8 @@ describe('PreviousNextButtonsComponent', () => { describe('onLastElement', () => { beforeEach(() => { - component.isFirst = false - component.isLast = true + component.listComponent.isFirstPage = false + component.listComponent.isLastPage = true fixture.detectChanges() }) diff --git a/libs/ui/layout/src/lib/previous-next-buttons/previous-next-buttons.component.stories.ts b/libs/ui/layout/src/lib/previous-next-buttons/previous-next-buttons.component.stories.ts new file mode 100644 index 0000000000..a65bafa6ac --- /dev/null +++ b/libs/ui/layout/src/lib/previous-next-buttons/previous-next-buttons.component.stories.ts @@ -0,0 +1,26 @@ +import { Meta, moduleMetadata, StoryObj } from '@storybook/angular' +import { PreviousNextButtonsComponent } from './previous-next-buttons.component' +import { MockListComponent } from '../pagination/pagination.component.stories' + +export default { + title: 'Layout/Pagination/PreviousNextButtonsComponent', + component: PreviousNextButtonsComponent, + parameters: { + backgrounds: { + default: 'dark', + }, + }, + decorators: [ + moduleMetadata({ + imports: [MockListComponent], + }), + ], +} as Meta + +export const Primary: StoryObj = { + render: () => ({ + template: ` + +`, + }), +} diff --git a/libs/ui/inputs/src/lib/previous-next-buttons/previous-next-buttons.component.ts b/libs/ui/layout/src/lib/previous-next-buttons/previous-next-buttons.component.ts similarity index 53% rename from libs/ui/inputs/src/lib/previous-next-buttons/previous-next-buttons.component.ts rename to libs/ui/layout/src/lib/previous-next-buttons/previous-next-buttons.component.ts index 9e95996105..889428f6cf 100644 --- a/libs/ui/inputs/src/lib/previous-next-buttons/previous-next-buttons.component.ts +++ b/libs/ui/layout/src/lib/previous-next-buttons/previous-next-buttons.component.ts @@ -1,11 +1,4 @@ -import { - ChangeDetectionStrategy, - Component, - EventEmitter, - Input, - Output, -} from '@angular/core' -import { ButtonComponent } from '../button/button.component' +import { Component, Input } from '@angular/core' import { NgIconComponent, provideIcons, @@ -15,12 +8,13 @@ import { matArrowBack, matArrowForward, } from '@ng-icons/material-icons/baseline' +import { Paginable } from '../paginable.interface' +import { ButtonComponent } from '@geonetwork-ui/ui/inputs' @Component({ selector: 'gn-ui-previous-next-buttons', templateUrl: './previous-next-buttons.component.html', styleUrls: ['./previous-next-buttons.component.css'], - changeDetection: ChangeDetectionStrategy.OnPush, standalone: true, imports: [ButtonComponent, NgIconComponent], providers: [ @@ -31,16 +25,5 @@ import { ], }) export class PreviousNextButtonsComponent { - @Input() isFirst: boolean - @Input() isLast: boolean - - @Output() directionButtonClicked: EventEmitter = new EventEmitter() - - previousButtonClicked() { - this.directionButtonClicked.next('previous') - } - - nextButtonClicked() { - this.directionButtonClicked.next('next') - } + @Input() listComponent: Paginable }