diff --git a/apps/datahub/src/app/app.module.ts b/apps/datahub/src/app/app.module.ts index d195c6e473..4cac62f591 100644 --- a/apps/datahub/src/app/app.module.ts +++ b/apps/datahub/src/app/app.module.ts @@ -25,11 +25,19 @@ import { RECORD_URL_TOKEN, } from '@geonetwork-ui/feature/search' import { + LinkCardComponent, THUMBNAIL_PLACEHOLDER, UiElementsModule, } from '@geonetwork-ui/ui/elements' -import { UiInputsModule } from '@geonetwork-ui/ui/inputs' -import { UiLayoutModule } from '@geonetwork-ui/ui/layout' +import { + PreviousNextButtonsComponent, + UiInputsModule, +} from '@geonetwork-ui/ui/inputs' +import { + BlockListComponent, + CarouselComponent, + UiLayoutModule, +} from '@geonetwork-ui/ui/layout' import { UiSearchModule } from '@geonetwork-ui/ui/search' import { getGlobalConfig, @@ -83,6 +91,7 @@ import { RecordApisComponent } from './record/record-apis/record-apis.component' import { MatTabsModule } from '@angular/material/tabs' import { UiWidgetsModule } from '@geonetwork-ui/ui/widgets' import { RecordUserFeedbacksComponent } from './record/record-user-feedbacks/record-user-feedbacks.component' +import { LetDirective } from '@ngrx/component' export const metaReducers: MetaReducer[] = !environment.production ? [] : [] @@ -150,6 +159,11 @@ export const metaReducers: MetaReducer[] = !environment.production ? [] : [] UiCatalogModule, MatTabsModule, UiWidgetsModule, + LinkCardComponent, + CarouselComponent, + BlockListComponent, + PreviousNextButtonsComponent, + LetDirective, ], providers: [ importProvidersFrom(FeatureAuthModule), diff --git a/apps/datahub/src/app/record/record-apis/record-apis.component.html b/apps/datahub/src/app/record/record-apis/record-apis.component.html index ee0d3c6694..a4bf38449f 100644 --- a/apps/datahub/src/app/record/record-apis/record-apis.component.html +++ b/apps/datahub/src/app/record/record-apis/record-apis.component.html @@ -1,37 +1,34 @@ -
+

record.metadata.api

- - - - +
+ + + +
close + >close +
1 + } + + updateView() { + this.changeDetector.detectChanges() + } + + get isFirstStep() { + return this.carousel?.isFirstStep + } + + get isLastStep() { + return this.carousel?.isLastStep + } + openRecordApiForm(link: DatasetServiceDistribution) { this.selectedApiLink = link this.setStyle(link) @@ -33,4 +64,12 @@ export class RecordApisComponent implements OnInit { this.maxHeight = link === undefined ? '0px' : '500px' this.opacity = link === undefined ? 0 : 1 } + + changeStepOrPage(direction: string) { + if (direction === 'next') { + this.carousel?.slideToNext() + } else { + this.carousel?.slideToPrevious() + } + } } diff --git a/apps/datahub/src/app/record/record-metadata/record-metadata.component.css b/apps/datahub/src/app/record/record-metadata/record-metadata.component.css index a92dcd44b6..e3a0105a26 100644 --- a/apps/datahub/src/app/record/record-metadata/record-metadata.component.css +++ b/apps/datahub/src/app/record/record-metadata/record-metadata.component.css @@ -33,18 +33,3 @@ .tab-header-label { @apply uppercase text-sm text-primary opacity-75 hover:text-primary-darker; } - -:host { - --container-outside-width: calc(50vw - 1024px / 2); -} -@media (max-width: 1024px) { - :host { - --container-outside-width: 1rem; - } -} - -:host ::ng-deep gn-ui-carousel { - display: block; - margin-left: calc(-1 * var(--container-outside-width)); - margin-right: calc(-1 * var(--container-outside-width)); -} diff --git a/apps/datahub/src/app/record/record-metadata/record-metadata.component.html b/apps/datahub/src/app/record/record-metadata/record-metadata.component.html index a339208c56..b55e6ed7e1 100644 --- a/apps/datahub/src/app/record/record-metadata/record-metadata.component.html +++ b/apps/datahub/src/app/record/record-metadata/record-metadata.component.html @@ -117,19 +117,20 @@ class="container-lg px-4 lg:mx-auto" *ngIf="displayDownload$ | async" > - +
-
+
- record.metadata.links -

- - +

- - + record.metadata.links +

+ +
+ + + + + + + + + + diff --git a/apps/datahub/src/app/record/record-otherlinks/record-otherlinks.component.spec.ts b/apps/datahub/src/app/record/record-otherlinks/record-otherlinks.component.spec.ts index 13ad395a38..bc4d48e632 100644 --- a/apps/datahub/src/app/record/record-otherlinks/record-otherlinks.component.spec.ts +++ b/apps/datahub/src/app/record/record-otherlinks/record-otherlinks.component.spec.ts @@ -1,12 +1,12 @@ import { ComponentFixture, TestBed } from '@angular/core/testing' -import { Subject } from 'rxjs' +import { BehaviorSubject } from 'rxjs' import { RecordOtherlinksComponent } from './record-otherlinks.component' import { MdViewFacade } from '@geonetwork-ui/feature/record' class MdViewFacadeMock { - otherLinks$ = new Subject() + otherLinks$ = new BehaviorSubject([]) } -describe('DataOtherlinksComponent', () => { +describe('RecordOtherlinksComponent', () => { let component: RecordOtherlinksComponent let fixture: ComponentFixture diff --git a/apps/datahub/src/app/record/record-otherlinks/record-otherlinks.component.ts b/apps/datahub/src/app/record/record-otherlinks/record-otherlinks.component.ts index d77e44adcc..97a39cf5a8 100644 --- a/apps/datahub/src/app/record/record-otherlinks/record-otherlinks.component.ts +++ b/apps/datahub/src/app/record/record-otherlinks/record-otherlinks.component.ts @@ -1,5 +1,12 @@ -import { Component, ChangeDetectionStrategy } from '@angular/core' +import { + AfterViewInit, + ChangeDetectionStrategy, + ChangeDetectorRef, + Component, + ViewChild, +} from '@angular/core' import { MdViewFacade } from '@geonetwork-ui/feature/record' +import { BlockListComponent, CarouselComponent } from '@geonetwork-ui/ui/layout' @Component({ selector: 'datahub-record-otherlinks', @@ -7,6 +14,45 @@ import { MdViewFacade } from '@geonetwork-ui/feature/record' styleUrls: ['./record-otherlinks.component.css'], changeDetection: ChangeDetectionStrategy.OnPush, }) -export class RecordOtherlinksComponent { - constructor(public facade: MdViewFacade) {} +export class RecordOtherlinksComponent implements AfterViewInit { + otherLinks$ = this.facade.otherLinks$ + + @ViewChild(CarouselComponent) carousel: CarouselComponent + @ViewChild(BlockListComponent) list: BlockListComponent + + constructor( + public facade: MdViewFacade, + private changeDetector: ChangeDetectorRef + ) {} + + get isFirstStepOrPage() { + return this.carousel?.isFirstStep ?? this.list?.isFirstPage ?? true + } + + get isLastStepOrPage() { + return this.carousel?.isLastStep ?? this.list?.isLastPage ?? false + } + + get hasPagination() { + return (this.carousel?.stepsCount || this.list?.pagesCount) > 1 + } + + changeStepOrPage(direction: string) { + if (direction === 'next') { + this.list?.nextPage() + this.carousel?.slideToNext() + } else { + this.carousel?.slideToPrevious() + this.list?.previousPage() + } + } + + updateView() { + this.changeDetector.detectChanges() + } + + ngAfterViewInit() { + // this is required to show the pagination correctly + this.changeDetector.detectChanges() + } } diff --git a/apps/datahub/src/app/record/record-page/record-page.component.html b/apps/datahub/src/app/record/record-page/record-page.component.html index 2f3fca3b97..139097e05a 100644 --- a/apps/datahub/src/app/record/record-page/record-page.component.html +++ b/apps/datahub/src/app/record/record-page/record-page.component.html @@ -1,4 +1,4 @@ -
+
diff --git a/libs/feature/record/src/lib/state/mdview.facade.ts b/libs/feature/record/src/lib/state/mdview.facade.ts index c8aa010cbb..37364de54d 100644 --- a/libs/feature/record/src/lib/state/mdview.facade.ts +++ b/libs/feature/record/src/lib/state/mdview.facade.ts @@ -8,7 +8,6 @@ import { DatavizConfigurationModel } from '@geonetwork-ui/common/domain/model/da import { CatalogRecord, UserFeedback, - UserFeedbackViewModel, } from '@geonetwork-ui/common/domain/model/record' import { AvatarServiceInterface } from '@geonetwork-ui/api/repository' diff --git a/libs/ui/elements/src/lib/link-card/link-card.component.html b/libs/ui/elements/src/lib/link-card/link-card.component.html index 8f87410c66..77ecdc3f1a 100644 --- a/libs/ui/elements/src/lib/link-card/link-card.component.html +++ b/libs/ui/elements/src/lib/link-card/link-card.component.html @@ -1,25 +1,43 @@ -
-

- {{ link.name }} -

-

- {{ link.description }} -

-

- {{ link.url }} -

-
-
- open_in_new -
+ +
+

+ {{ link.name }} +

+

+ {{ link.description }} +

+

+ {{ link.url }} +

+
+
+ open_in_new +
+
+ +
+

+ {{ link.name || link.description }} +

+ open_in_new +
+
diff --git a/libs/ui/elements/src/lib/link-card/link-card.component.spec.ts b/libs/ui/elements/src/lib/link-card/link-card.component.spec.ts index 1c84cd4b94..95bcdc690e 100644 --- a/libs/ui/elements/src/lib/link-card/link-card.component.spec.ts +++ b/libs/ui/elements/src/lib/link-card/link-card.component.spec.ts @@ -1,7 +1,5 @@ import { NO_ERRORS_SCHEMA } from '@angular/core' import { ComponentFixture, TestBed } from '@angular/core/testing' -import { MatIconModule } from '@angular/material/icon' -import { TranslateModule } from '@ngx-translate/core' import { LinkCardComponent } from './link-card.component' describe('LinkCardComponent', () => { @@ -10,9 +8,8 @@ describe('LinkCardComponent', () => { beforeEach(async () => { await TestBed.configureTestingModule({ - declarations: [LinkCardComponent], schemas: [NO_ERRORS_SCHEMA], - imports: [MatIconModule, TranslateModule.forRoot()], + imports: [LinkCardComponent], }).compileComponents() }) diff --git a/libs/ui/elements/src/lib/link-card/link-card.component.stories.ts b/libs/ui/elements/src/lib/link-card/link-card.component.stories.ts index 959b3cd1fe..3597360c7c 100644 --- a/libs/ui/elements/src/lib/link-card/link-card.component.stories.ts +++ b/libs/ui/elements/src/lib/link-card/link-card.component.stories.ts @@ -1,8 +1,3 @@ -import { - TRANSLATE_DEFAULT_CONFIG, - UtilI18nModule, -} from '@geonetwork-ui/util/i18n' -import { TranslateModule } from '@ngx-translate/core' import { applicationConfig, componentWrapperDecorator, @@ -13,18 +8,13 @@ import { import { LinkCardComponent } from './link-card.component' import { BrowserAnimationsModule } from '@angular/platform-browser/animations' import { importProvidersFrom } from '@angular/core' -import { MatIcon } from '@angular/material/icon' export default { title: 'Elements/LinkCardComponent', component: LinkCardComponent, decorators: [ moduleMetadata({ - declarations: [MatIcon], - imports: [ - UtilI18nModule, - TranslateModule.forRoot(TRANSLATE_DEFAULT_CONFIG), - ], + imports: [LinkCardComponent], }), applicationConfig({ providers: [importProvidersFrom(BrowserAnimationsModule)], @@ -37,6 +27,7 @@ export default { export const Primary: StoryObj = { args: { + compact: false, link: { type: 'link', name: 'Consulter sur Géoclip', diff --git a/libs/ui/elements/src/lib/link-card/link-card.component.ts b/libs/ui/elements/src/lib/link-card/link-card.component.ts index 8cea45b83e..78e8bd0622 100644 --- a/libs/ui/elements/src/lib/link-card/link-card.component.ts +++ b/libs/ui/elements/src/lib/link-card/link-card.component.ts @@ -1,12 +1,24 @@ import { Component, ChangeDetectionStrategy, Input } from '@angular/core' import { DatasetDistribution } from '@geonetwork-ui/common/domain/model/record' +import { MatIconModule } from '@angular/material/icon' +import { CommonModule } from '@angular/common' @Component({ selector: 'gn-ui-link-card', templateUrl: './link-card.component.html', styleUrls: ['./link-card.component.css'], changeDetection: ChangeDetectionStrategy.OnPush, + standalone: true, + imports: [CommonModule, MatIconModule], }) export class LinkCardComponent { @Input() link: DatasetDistribution + @Input() compact = false + + get title() { + if (this.link.name && this.link.description) { + return `${this.link.name} | ${this.link.description}` + } + return this.link.name || this.link.description || '' + } } diff --git a/libs/ui/elements/src/lib/ui-elements.module.ts b/libs/ui/elements/src/lib/ui-elements.module.ts index 64373c318c..58d85ece10 100644 --- a/libs/ui/elements/src/lib/ui-elements.module.ts +++ b/libs/ui/elements/src/lib/ui-elements.module.ts @@ -57,7 +57,6 @@ import { TimeSincePipe } from './user-feedback-item/time-since.pipe' DownloadItemComponent, DownloadsListComponent, ApiCardComponent, - LinkCardComponent, RelatedRecordCardComponent, MetadataContactComponent, MetadataCatalogComponent, @@ -80,7 +79,6 @@ import { TimeSincePipe } from './user-feedback-item/time-since.pipe' DownloadItemComponent, DownloadsListComponent, ApiCardComponent, - LinkCardComponent, RelatedRecordCardComponent, MetadataContactComponent, MetadataCatalogComponent, diff --git a/libs/ui/inputs/src/index.ts b/libs/ui/inputs/src/index.ts index 88f97f9216..1baf8d6f45 100644 --- a/libs/ui/inputs/src/index.ts +++ b/libs/ui/inputs/src/index.ts @@ -19,3 +19,4 @@ export * from './lib/text-area/text-area.component' export * from './lib/text-input/text-input.component' export * from './lib/ui-inputs.module' export * from './lib/viewport-intersector/viewport-intersector.component' +export * from './lib/previous-next-buttons/previous-next-buttons.component' diff --git a/libs/ui/inputs/src/lib/previous-next-buttons/previous-next-buttons.component.css b/libs/ui/inputs/src/lib/previous-next-buttons/previous-next-buttons.component.css new file mode 100644 index 0000000000..14ad317ff0 --- /dev/null +++ b/libs/ui/inputs/src/lib/previous-next-buttons/previous-next-buttons.component.css @@ -0,0 +1,6 @@ +:host { + --gn-ui-button-rounded: 100%; + --gn-ui-button-width: 8px; + --gn-ui-button-height: 8px; + --gn-ui-button-padding: 12px; +} diff --git a/libs/ui/inputs/src/lib/previous-next-buttons/previous-next-buttons.component.html b/libs/ui/inputs/src/lib/previous-next-buttons/previous-next-buttons.component.html new file mode 100644 index 0000000000..d92b9276c0 --- /dev/null +++ b/libs/ui/inputs/src/lib/previous-next-buttons/previous-next-buttons.component.html @@ -0,0 +1,26 @@ +
+ + + arrow_back + + + + + arrow_forward + + +
diff --git a/libs/ui/inputs/src/lib/previous-next-buttons/previous-next-buttons.component.spec.ts b/libs/ui/inputs/src/lib/previous-next-buttons/previous-next-buttons.component.spec.ts new file mode 100644 index 0000000000..4722325bc7 --- /dev/null +++ b/libs/ui/inputs/src/lib/previous-next-buttons/previous-next-buttons.component.spec.ts @@ -0,0 +1,73 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing' +import { MatIconModule } from '@angular/material/icon' + +import { PreviousNextButtonsComponent } from './previous-next-buttons.component' +import { TranslateModule } from '@ngx-translate/core' +import { By } from '@angular/platform-browser' +import { DebugElement } from '@angular/core' + +describe('PreviousNextButtonsComponent', () => { + let component: PreviousNextButtonsComponent + let fixture: ComponentFixture + let compiled: DebugElement + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [ + MatIconModule, + PreviousNextButtonsComponent, + TranslateModule.forRoot(), + ], + }).compileComponents() + }) + + beforeEach(() => { + fixture = TestBed.createComponent(PreviousNextButtonsComponent) + component = fixture.componentInstance + compiled = fixture.debugElement + }) + + it('should create', () => { + expect(component).toBeTruthy() + }) + + describe('onFirstElement', () => { + beforeEach(() => { + component.isFirst = true + component.isLast = false + fixture.detectChanges() + }) + + it('previous button should be disabled', () => { + const previousButton = compiled.query( + By.css('[data-test="previousButton"]') + ) + expect(previousButton.attributes['ng-reflect-disabled']).toEqual('true') + }) + + it("next button shouldn't be disabled", () => { + const nextButton = compiled.query(By.css('[data-test="nextButton"]')) + expect(nextButton.attributes['ng-reflect-disabled']).toEqual('false') + }) + }) + + describe('onLastElement', () => { + beforeEach(() => { + component.isFirst = false + component.isLast = true + fixture.detectChanges() + }) + + it('previous button should be disabled', () => { + const previousButton = compiled.query( + By.css('[data-test="previousButton"]') + ) + expect(previousButton.attributes['ng-reflect-disabled']).toEqual('false') + }) + + it("next button shouldn't be disabled", () => { + const nextButton = compiled.query(By.css('[data-test="nextButton"]')) + expect(nextButton.attributes['ng-reflect-disabled']).toEqual('true') + }) + }) +}) 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 new file mode 100644 index 0000000000..efc14c7116 --- /dev/null +++ b/libs/ui/inputs/src/lib/previous-next-buttons/previous-next-buttons.component.stories.ts @@ -0,0 +1,39 @@ +import { Meta, moduleMetadata, StoryObj } from '@storybook/angular' +import { PreviousNextButtonsComponent } from './previous-next-buttons.component' +import { TranslateModule } from '@ngx-translate/core' +import { MatIconModule } from '@angular/material/icon' +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), + MatIconModule, + ], + }), + ], +} as Meta + +export const Primary: StoryObj = { + args: { + isFirst: true, + isLast: false, + }, + render: (args) => ({ + props: args, + template: + '', + }), +} diff --git a/libs/ui/inputs/src/lib/previous-next-buttons/previous-next-buttons.component.ts b/libs/ui/inputs/src/lib/previous-next-buttons/previous-next-buttons.component.ts new file mode 100644 index 0000000000..3546968262 --- /dev/null +++ b/libs/ui/inputs/src/lib/previous-next-buttons/previous-next-buttons.component.ts @@ -0,0 +1,32 @@ +import { + ChangeDetectionStrategy, + Component, + EventEmitter, + Input, + Output, +} from '@angular/core' +import { ButtonComponent } from '../button/button.component' +import { MatIconModule } from '@angular/material/icon' + +@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, MatIconModule], +}) +export class PreviousNextButtonsComponent { + @Input() isFirst: boolean + @Input() isLast: boolean + + @Output() directionButtonClicked: EventEmitter = new EventEmitter() + + previousButtonClicked() { + this.directionButtonClicked.next('previous') + } + + nextButtonClicked() { + this.directionButtonClicked.next('next') + } +} diff --git a/libs/ui/layout/src/index.ts b/libs/ui/layout/src/index.ts index 9dbf8aec71..358c081b9f 100644 --- a/libs/ui/layout/src/index.ts +++ b/libs/ui/layout/src/index.ts @@ -6,4 +6,5 @@ export * from './lib/form-field-wrapper/form-field-wrapper.component' export * from './lib/interactive-table/interactive-table-column/interactive-table-column.component' export * from './lib/interactive-table/interactive-table.component' export * from './lib/sticky-header/sticky-header.component' +export * from './lib/block-list/block-list.component' export * from './lib/ui-layout.module' diff --git a/libs/ui/layout/src/lib/block-list/block-list.component.css b/libs/ui/layout/src/lib/block-list/block-list.component.css new file mode 100644 index 0000000000..0d37f18585 --- /dev/null +++ b/libs/ui/layout/src/lib/block-list/block-list.component.css @@ -0,0 +1,23 @@ +:host .block-list-container ::ng-deep > * { + flex-shrink: 0; +} + +:host { + position: relative; +} + +.list-page-dot { + width: 6px; + height: 6px; + border-radius: 6px; + position: relative; +} + +.list-page-dot:after { + content: ''; + position: absolute; + left: -7px; + top: -7px; + width: 20px; + height: 20px; +} diff --git a/libs/ui/layout/src/lib/block-list/block-list.component.html b/libs/ui/layout/src/lib/block-list/block-list.component.html new file mode 100644 index 0000000000..17e706ddd6 --- /dev/null +++ b/libs/ui/layout/src/lib/block-list/block-list.component.html @@ -0,0 +1,20 @@ +
+ +
+
+ +
diff --git a/libs/ui/layout/src/lib/block-list/block-list.component.spec.ts b/libs/ui/layout/src/lib/block-list/block-list.component.spec.ts new file mode 100644 index 0000000000..2b28278dde --- /dev/null +++ b/libs/ui/layout/src/lib/block-list/block-list.component.spec.ts @@ -0,0 +1,173 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing' +import { BlockListComponent } from './block-list.component' +import { Component, Input } from '@angular/core' +import { By } from '@angular/platform-browser' + +@Component({ + template: ` +
+
`, +}) +class BlockListWrapperComponent { + @Input() blocks = [1, 2, 3, 4, 5, 6, 7] +} + +describe('BlockListComponent', () => { + let component: BlockListComponent + let fixture: ComponentFixture + let blockEls: HTMLElement[] + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [BlockListWrapperComponent], + imports: [BlockListComponent], + }).compileComponents() + fixture = TestBed.createComponent(BlockListWrapperComponent) + component = fixture.debugElement.query( + By.directive(BlockListComponent) + ).componentInstance + fixture.detectChanges() + blockEls = fixture.debugElement + .queryAll(By.css('.block')) + .map((el) => el.nativeElement) + }) + + it('should create', () => { + expect(component).toBeTruthy() + }) + + describe('pages computation', () => { + it('shows 5 items per page initially', () => { + const blocksVisibility = blockEls.map((el) => el.style.display !== 'none') + expect(blocksVisibility).toEqual([ + true, + true, + true, + true, + true, + false, + false, + ]) + }) + describe('click on step', () => { + beforeEach(() => { + component.goToPage(1) + }) + it('updates visibility', () => { + const blocksVisibility = blockEls.map( + (el) => el.style.display !== 'none' + ) + expect(blocksVisibility).toEqual([ + false, + false, + false, + false, + false, + true, + true, + ]) + }) + it('emits the selected page', () => { + expect(component['currentPage']).toEqual(1) + }) + }) + describe('custom page size', () => { + beforeEach(() => { + component.pageSize = 3 + component.goToPage(3) + fixture.detectChanges() + }) + it('updates visibility', () => { + const blocksVisibility = blockEls.map( + (el) => el.style.display !== 'none' + ) + expect(blocksVisibility).toEqual([ + false, + false, + false, + false, + false, + false, + true, + ]) + }) + }) + }) + + describe('previousPage', () => { + beforeEach(() => { + component.pageSize = 2 + component.goToPage(2) + component.previousPage() + }) + it('changes to previous page', () => { + expect(component['currentPage']).toEqual(1) + }) + }) + + describe('nextPage', () => { + beforeEach(() => { + component.pageSize = 2 + component.goToPage(1) + component.nextPage() + }) + it('changes to next page', () => { + expect(component['currentPage']).toEqual(2) + }) + }) + + describe('isFirstPage', () => { + beforeEach(() => { + component.pageSize = 3 + }) + it('returns true if the current page is the first one', () => { + expect(component.isFirstPage).toBe(true) + }) + it('returns false if the current page is not the first one', () => { + component.goToPage(1) + expect(component.isFirstPage).toBe(false) + }) + }) + + describe('isLastPage', () => { + beforeEach(() => { + component.pageSize = 3 + }) + it('returns true if the current page is the last one', () => { + component.goToPage(2) + expect(component.isLastPage).toBe(true) + }) + it('returns false if the current page is not the last one', () => { + component.goToPage(1) + expect(component.isLastPage).toBe(false) + }) + }) + + describe('set initial height as min height, keeps value when height changes', () => { + beforeEach(() => { + Object.defineProperties(component.blockContainer.nativeElement, { + clientHeight: { + value: 150, + }, + }) + fixture.detectChanges() + component.ngAfterViewInit() + Object.defineProperties(component.blockContainer.nativeElement, { + clientHeight: { + value: 50, + }, + }) + fixture.detectChanges() + }) + it('sets the min height of the container according to its initial content', () => { + expect(component.blockContainer.nativeElement.style.minHeight).toBe( + '150px' + ) + }) + }) +}) diff --git a/libs/ui/layout/src/lib/block-list/block-list.component.stories.ts b/libs/ui/layout/src/lib/block-list/block-list.component.stories.ts new file mode 100644 index 0000000000..31f06fdd74 --- /dev/null +++ b/libs/ui/layout/src/lib/block-list/block-list.component.stories.ts @@ -0,0 +1,44 @@ +import type { Meta, StoryObj } from '@storybook/angular' +import { BlockListComponent } from './block-list.component' +import { componentWrapperDecorator } from '@storybook/angular' + +const meta: Meta = { + component: BlockListComponent, + title: 'Layout/BlockListComponent', + decorators: [ + componentWrapperDecorator( + (story) => + `
${story}
` + ), + ], +} +export default meta +type Story = StoryObj< + BlockListComponent & { + blockCount: number + } +> + +export const Primary: Story = { + args: { + pageSize: 5, + blockCount: 17, + }, + render: (args) => ({ + props: { + ...args, + blockList: new Array(args.blockCount).fill(0).map((_, i) => i + 1), + }, + template: ` + +
+ Box {{ block }} +
+
+`, + }), +} diff --git a/libs/ui/layout/src/lib/block-list/block-list.component.ts b/libs/ui/layout/src/lib/block-list/block-list.component.ts new file mode 100644 index 0000000000..5a0937c761 --- /dev/null +++ b/libs/ui/layout/src/lib/block-list/block-list.component.ts @@ -0,0 +1,84 @@ +import { + AfterViewInit, + ChangeDetectionStrategy, + ChangeDetectorRef, + Component, + ContentChildren, + ElementRef, + Input, + QueryList, + ViewChild, +} from '@angular/core' +import { CommonModule } from '@angular/common' + +@Component({ + selector: 'gn-ui-block-list', + templateUrl: './block-list.component.html', + styleUrls: ['./block-list.component.css'], + changeDetection: ChangeDetectionStrategy.OnPush, + standalone: true, + imports: [CommonModule], +}) +export class BlockListComponent implements AfterViewInit { + @Input() pageSize = 5 + @Input() containerClass = '' + @Input() paginationContainerClass = 'w-full bottom-0 top-auto' + @ContentChildren('block', { read: ElementRef }) blocks: QueryList< + ElementRef + > + @ViewChild('blockContainer') blockContainer: ElementRef + + protected minHeight = 0 + + protected currentPage = 0 + protected get pages() { + return new Array(this.pagesCount).fill(0).map((_, i) => i) + } + + get isFirstPage() { + return this.currentPage === 0 + } + get isLastPage() { + return this.currentPage === this.pagesCount - 1 + } + get pagesCount() { + return this.blocks ? Math.ceil(this.blocks.length / this.pageSize) : 1 + } + + constructor(private changeDetector: ChangeDetectorRef) {} + + ngAfterViewInit() { + this.blocks.changes.subscribe(this.refreshBlocksVisibility) + this.refreshBlocksVisibility() + + // we store the first height as the min-height of the list container + this.minHeight = this.blockContainer.nativeElement.clientHeight + this.changeDetector.detectChanges() + } + + protected refreshBlocksVisibility = () => { + this.blocks.forEach((block, index) => { + block.nativeElement.style.display = + index >= this.currentPage * this.pageSize && + index < (this.currentPage + 1) * this.pageSize + ? null + : 'none' + }) + } + + public goToPage(index: number) { + this.currentPage = Math.max(Math.min(index, this.pagesCount - 1), 0) + this.changeDetector.detectChanges() + this.refreshBlocksVisibility() + } + + public previousPage() { + if (this.isFirstPage) return + this.goToPage(this.currentPage - 1) + } + + public nextPage() { + if (this.isLastPage) return + this.goToPage(this.currentPage + 1) + } +} diff --git a/libs/ui/layout/src/lib/carousel/carousel.component.css b/libs/ui/layout/src/lib/carousel/carousel.component.css index ed93f493c4..e751dea020 100644 --- a/libs/ui/layout/src/lib/carousel/carousel.component.css +++ b/libs/ui/layout/src/lib/carousel/carousel.component.css @@ -1,20 +1,23 @@ :host .carousel-container ::ng-deep > * { flex-shrink: 0; } + :host { position: relative; } + .carousel-step-dot { width: 6px; height: 6px; border-radius: 6px; position: relative; } + .carousel-step-dot:after { content: ''; position: absolute; - left: -4px; - top: -4px; - width: 14px; - height: 14px; + 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 52034ab817..0c2ae552d9 100644 --- a/libs/ui/layout/src/lib/carousel/carousel.component.html +++ b/libs/ui/layout/src/lib/carousel/carousel.component.html @@ -1,17 +1,17 @@ -
-