Skip to content

Commit

Permalink
Merge pull request #729 from geonetwork/DH-read-more-description
Browse files Browse the repository at this point in the history
[UI]: Add component max-lines with toggle
  • Loading branch information
Angi-Kinas authored Dec 20, 2023
2 parents 95f0151 + 2c3436c commit ef80f77
Show file tree
Hide file tree
Showing 18 changed files with 338 additions and 34 deletions.
38 changes: 24 additions & 14 deletions apps/datahub-e2e/src/e2e/datasetDetailPage.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,9 @@ describe('dataset pages', () => {
.find('[id="about"]')
.find('gn-ui-metadata-info')
.find('gn-ui-content-ghost')
.find('gn-ui-max-lines')
.children('div')
.children('div')
.children('p')
.should(($element) => {
const text = $element.text().trim()
Expand Down Expand Up @@ -146,8 +149,11 @@ describe('dataset pages', () => {
cy.get('datahub-record-metadata')
.find('[id="about"]')
.find('gn-ui-metadata-info')
.find('gn-ui-content-ghost')
.find('gn-ui-max-lines')
.children('div')
.children('div')
.children('div')
.eq(1)
.children('gn-ui-badge')
.should('have.length.gt', 0)
})
Expand Down Expand Up @@ -193,22 +199,26 @@ describe('dataset pages', () => {
cy.get('datahub-record-metadata')
.find('[id="about"]')
.find('gn-ui-metadata-info')
.find('gn-ui-content-ghost')
.find('gn-ui-max-lines')
.children('div')
.contains('Read more')
.click()

cy.get('datahub-record-metadata')
.find('gn-ui-badge')
.children('div')
.eq(1)
.children('gn-ui-badge')
.first()
.as('keyword')

cy.get('@keyword')
.children('div')
.then((key) => {
keyword = key.text().toUpperCase()
cy.get('@keyword').click()
cy.url().should('include', '/search?q=')
cy.get('gn-ui-fuzzy-search')
.find('input')
.should('have.value', keyword)
})
cy.get('@keyword').then((key) => {
keyword = key.text().toUpperCase()
cy.get('@keyword').first().click()
cy.url().should('include', '/search?q=')
cy.get('gn-ui-fuzzy-search')
.find('input')
.should('have.value', keyword)
})
})
})
})
Expand Down Expand Up @@ -443,7 +453,7 @@ describe('dataset pages', () => {
.find('gn-ui-copy-text-button')
.find('button')
.first()
.click({ force: true })
.realClick()
// attempt to make the whole page focused
cy.get('body').focus()
cy.get('body').realClick()
Expand Down
7 changes: 7 additions & 0 deletions apps/datahub/src/test-setup.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,9 @@
import 'jest-preset-angular/setup-jest'
import '../../../jest.setup'

class ResizeObserverMock {
observe = jest.fn()
unobserve = jest.fn()
}

;(window as any).ResizeObserver = ResizeObserverMock
Empty file.
15 changes: 15 additions & 0 deletions libs/ui/elements/src/lib/max-lines/max-lines.component.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<div
#container
class="max-lines overflow-hidden transition-[max-height] duration-300"
[ngClass]="isExpanded ? 'ease-in' : 'ease-out'"
[style.maxHeight]="maxHeight"
>
<ng-content></ng-content>
</div>
<div
*ngIf="showToggleButton"
(click)="toggleDisplay()"
class="text-secondary cursor-pointer pt-2.5"
>
{{ (isExpanded ? 'ui.readLess' : 'ui.readMore') | translate }}
</div>
122 changes: 122 additions & 0 deletions libs/ui/elements/src/lib/max-lines/max-lines.component.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
import { ComponentFixture, TestBed } from '@angular/core/testing'

import { MaxLinesComponent } from './max-lines.component'
import { Component, importProvidersFrom } from '@angular/core'
import { TranslateModule } from '@ngx-translate/core'

@Component({
template: `
<gn-ui-max-lines [maxLines]="maxLines">
<div class="test-content" style="height: 70px; max-height:80px;">
<p style="height: 40px;">Lorem ipsum dolor sit amet</p>
</div>
</gn-ui-max-lines>
`,
})
class TestHostComponent {
maxLines: number
}
describe('MaxLinesComponent', () => {
let fixture: ComponentFixture<TestHostComponent>
let hostComponent: TestHostComponent
let maxLinesComponent: MaxLinesComponent

beforeEach(() => {
TestBed.configureTestingModule({
imports: [TranslateModule.forRoot()],
providers: [importProvidersFrom(TranslateModule.forRoot())],
declarations: [MaxLinesComponent, TestHostComponent],
})
fixture = TestBed.createComponent(TestHostComponent)
hostComponent = fixture.componentInstance
maxLinesComponent = fixture.debugElement.children[0].componentInstance
})

it('should create', () => {
fixture.detectChanges()
expect(maxLinesComponent).toBeTruthy()
})

it('should render content correctly', () => {
hostComponent.maxLines = 10
fixture.detectChanges()

const maxLinesElement: HTMLElement =
fixture.nativeElement.querySelector('.max-lines')
expect(maxLinesElement.childNodes[0].textContent).toBe(
'Lorem ipsum dolor sit amet'
)
})

describe('should adjust maxHeight based on content height', () => {
const contentHeight = 120
beforeEach(() => {
jest
.spyOn(
fixture.nativeElement.querySelector('.test-content'),
'getBoundingClientRect'
)
.mockReturnValueOnce({
left: 100,
top: 50,
right: 20,
bottom: 10,
x: 30,
y: 40,
widht: 150,
height: contentHeight,
})
})
it('use content height if content height is smaller than max space', () => {
hostComponent.maxLines = 10

fixture.detectChanges()

expect(maxLinesComponent.maxHeight).toBe(`${contentHeight}px`)
})

it('use max space height if content height is bigger than max space', () => {
hostComponent.maxLines = 2

const contentElement =
fixture.nativeElement.querySelector('.test-content')
fixture.detectChanges()

const maxSpace =
maxLinesComponent.getLineHeight(contentElement) *
maxLinesComponent.maxLines

expect(maxLinesComponent.maxHeight).toBe(`${maxSpace}px`)
})

it('should show "Show More" button for long content', () => {
hostComponent.maxLines = 2

fixture.detectChanges()

expect(maxLinesComponent.showToggleButton).toBeTruthy()
})

it('should toggle display when "Show More" button is clicked', () => {
hostComponent.maxLines = 2

const contentElement =
fixture.nativeElement.querySelector('.test-content')
fixture.detectChanges()

maxLinesComponent.toggleDisplay()

expect(maxLinesComponent.isExpanded).toBeTruthy()
expect(maxLinesComponent.maxHeight).toBe(
`${
maxLinesComponent.maxLines *
maxLinesComponent.getLineHeight(contentElement)
}px`
)
})
})

afterEach(() => {
fixture.destroy()
})
})
40 changes: 40 additions & 0 deletions libs/ui/elements/src/lib/max-lines/max-lines.component.stories.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import {
applicationConfig,
componentWrapperDecorator,
Meta,
moduleMetadata,
StoryObj,
} from '@storybook/angular'
import { MaxLinesComponent } from './max-lines.component'

export default {
title: 'Elements/MaxLinesComponent',
component: MaxLinesComponent,
decorators: [
moduleMetadata({
declarations: [MaxLinesComponent],
imports: [],
}),
applicationConfig({
providers: [],
}),
componentWrapperDecorator(
(story) => `<div style="max-width: 800px">${story}</div>`
),
],
} as Meta<MaxLinesComponent>

const largeContent = `<div><p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Proin purus elit, tincidunt et gravida sit amet, mattis eget orci. Suspendisse dignissim magna sed neque rutrum lobortis. Aenean vitae quam sapien. Phasellus eleifend tortor ac imperdiet tristique. Curabitur aliquet mauris tristique, iaculis est sit amet, pulvinar ipsum. Maecenas lacinia varius felis sit amet tempor. Curabitur pulvinar ipsum eros, quis accumsan odio hendrerit sit amet.
Vestibulum placerat posuere lectus, sed lacinia orci sagittis consectetur. Duis eget eros consectetur, pretium nulla semper, pretium justo. Nullam facilisis maximus ipsum, a tempus erat eleifend non. Nulla nec lorem sed lorem porttitor ornare. Aliquam condimentum ante at laoreet dignissim. Vestibulum vel laoreet libero. Nam finibus augue ut ligula vulputate porta. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Aliquam erat volutpat. Nunc lorem nunc, interdum sed leo vel, vestibulum venenatis diam. Nam eget dignissim purus. Cras convallis leo sed porta tristique.</p></div>`

export const Primary: StoryObj<MaxLinesComponent> = {
args: {
maxLines: 6,
},
render: (args) => ({
template: `<div>
<gn-ui-max-lines [maxLines]=${args.maxLines}>${largeContent}</gn-ui-max-lines>
</div>`,
}),
}
83 changes: 83 additions & 0 deletions libs/ui/elements/src/lib/max-lines/max-lines.component.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import {
Component,
Input,
ElementRef,
ChangeDetectionStrategy,
AfterViewInit,
ViewChild,
OnDestroy,
ChangeDetectorRef,
} from '@angular/core'

@Component({
selector: 'gn-ui-max-lines',
templateUrl: './max-lines.component.html',
styleUrls: ['./max-lines.component.css'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class MaxLinesComponent implements AfterViewInit, OnDestroy {
@Input() maxLines = 6

isExpanded = false
maxHeight = ''
showToggleButton = false
observer: ResizeObserver

@ViewChild('container') container!: ElementRef

constructor(private cdr: ChangeDetectorRef) {}

ngAfterViewInit() {
this.calculateMaxHeight()

this.observer = new ResizeObserver((mutations) => {
mutations.forEach(() => {
this.calculateMaxHeight()
})
})

this.observer.observe(this.container.nativeElement.children[0])
}

toggleDisplay() {
this.isExpanded = !this.isExpanded
this.calculateMaxHeight()
}

calculateMaxHeight() {
const containerElement = this.container.nativeElement
const contentElement = containerElement.children[0]
const contentHeight = contentElement.getBoundingClientRect().height

if (contentHeight) {
if (contentHeight > this.maxLines * this.getLineHeight(contentElement)) {
this.showToggleButton = true

this.maxHeight = this.isExpanded
? `${contentHeight}px`
: `${this.maxLines * this.getLineHeight(contentElement)}px`
} else {
this.showToggleButton = false
this.maxHeight = `${contentHeight}px`
}
containerElement.setAttribute(
'style',
`max-height: ${this.maxHeight}; overflow: hidden`
)

this.cdr.detectChanges()
}
}

getLineHeight(element: HTMLElement): number {
const computedStyle = window.getComputedStyle(element)
const lineHeight = parseFloat(computedStyle.lineHeight)
const fontSize = parseFloat(computedStyle.fontSize || '14')
const result = isNaN(lineHeight) ? fontSize * 1.2 : lineHeight // Use a default if line height is not specified
return result
}

ngOnDestroy(): void {
this.observer.unobserve(this.container.nativeElement.children[0])
}
}
42 changes: 22 additions & 20 deletions libs/ui/elements/src/lib/metadata-info/metadata-info.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -6,29 +6,31 @@
</p>
<div class="mb-6 md-description sm:mb-4 sm:pr-16">
<gn-ui-content-ghost ghostClass="h-32" [showContent]="fieldReady('abstract')">
<p
class="whitespace-pre-line break-words"
[innerHTML]="metadata.abstract"
*ngIf="metadata.abstract"
></p>
<gn-ui-max-lines [maxLines]="6" *ngIf="metadata.abstract">
<div>
<p
class="whitespace-pre-line break-words sm:mb-4 sm:pr-16"
[innerHTML]="metadata.abstract"
></p>
<ng-container *ngIf="metadata.keywords?.length">
<p class="mb-3 font-medium text-primary text-sm" translate>
record.metadata.keywords
</p>
<div class="sm:pb-4 sm:pr-16">
<gn-ui-badge
class="inline-block mr-2 mb-2 lowercase"
(click)="onKeywordClick(keyword)"
[clickable]="true"
*ngFor="let keyword of metadata.keywords"
>{{ keyword }}</gn-ui-badge
>
</div>
</ng-container>
</div>
</gn-ui-max-lines>
</gn-ui-content-ghost>
</div>

<ng-container *ngIf="metadata.keywords?.length">
<p class="mb-3 font-medium text-primary text-sm" translate>
record.metadata.keywords
</p>
<div class="mb-9 sm:mb-16">
<gn-ui-badge
class="inline-block mr-2 mb-2 lowercase"
(click)="onKeywordClick(keyword)"
[clickable]="true"
*ngFor="let keyword of metadata.keywords"
>{{ keyword }}</gn-ui-badge
>
</div>
</ng-container>

<gn-ui-expandable-panel
class="metadata-origin"
*ngIf="
Expand Down
Loading

0 comments on commit ef80f77

Please sign in to comment.