Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[UI]: Add component max-lines with toggle #729

Merged
merged 9 commits into from
Dec 20, 2023
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
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>
119 changes: 119 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,119 @@
import { ComponentFixture, TestBed } from '@angular/core/testing'

import { MaxLinesComponent } from './max-lines.component'
import { Component } from '@angular/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({
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
2 changes: 2 additions & 0 deletions libs/ui/elements/src/lib/ui-elements.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import { AvatarComponent } from './avatar/avatar.component'
import { UserPreviewComponent } from './user-preview/user-preview.component'
import { GnUiLinkifyDirective } from './metadata-info/linkify.directive'
import { PaginationButtonsComponent } from './pagination-buttons/pagination-buttons.component'
import { MaxLinesComponent } from './max-lines/max-lines.component'

@NgModule({
imports: [
Expand Down Expand Up @@ -61,6 +62,7 @@ import { PaginationButtonsComponent } from './pagination-buttons/pagination-butt
UserPreviewComponent,
GnUiLinkifyDirective,
PaginationButtonsComponent,
MaxLinesComponent,
],
exports: [
MetadataInfoComponent,
Expand Down
Loading
Loading