diff --git a/apps/datahub-e2e/src/e2e/datasetDetailPage.cy.ts b/apps/datahub-e2e/src/e2e/datasetDetailPage.cy.ts
index 0fd48c48db..a13e935c01 100644
--- a/apps/datahub-e2e/src/e2e/datasetDetailPage.cy.ts
+++ b/apps/datahub-e2e/src/e2e/datasetDetailPage.cy.ts
@@ -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()
@@ -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)
})
@@ -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)
+ })
})
})
})
@@ -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()
diff --git a/apps/datahub/src/test-setup.ts b/apps/datahub/src/test-setup.ts
index 2de8264de0..5626c49f29 100644
--- a/apps/datahub/src/test-setup.ts
+++ b/apps/datahub/src/test-setup.ts
@@ -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
diff --git a/libs/ui/elements/src/lib/max-lines/max-lines.component.css b/libs/ui/elements/src/lib/max-lines/max-lines.component.css
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/libs/ui/elements/src/lib/max-lines/max-lines.component.html b/libs/ui/elements/src/lib/max-lines/max-lines.component.html
new file mode 100644
index 0000000000..42dee99803
--- /dev/null
+++ b/libs/ui/elements/src/lib/max-lines/max-lines.component.html
@@ -0,0 +1,15 @@
+
+
+
+
+ {{ (isExpanded ? 'ui.readLess' : 'ui.readMore') | translate }}
+
diff --git a/libs/ui/elements/src/lib/max-lines/max-lines.component.spec.ts b/libs/ui/elements/src/lib/max-lines/max-lines.component.spec.ts
new file mode 100644
index 0000000000..dc28d6869c
--- /dev/null
+++ b/libs/ui/elements/src/lib/max-lines/max-lines.component.spec.ts
@@ -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: `
+
+
+
Lorem ipsum dolor sit amet
+
+
+ `,
+})
+class TestHostComponent {
+ maxLines: number
+}
+describe('MaxLinesComponent', () => {
+ let fixture: ComponentFixture
+ 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()
+ })
+})
diff --git a/libs/ui/elements/src/lib/max-lines/max-lines.component.stories.ts b/libs/ui/elements/src/lib/max-lines/max-lines.component.stories.ts
new file mode 100644
index 0000000000..07c296f742
--- /dev/null
+++ b/libs/ui/elements/src/lib/max-lines/max-lines.component.stories.ts
@@ -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) => `${story}
`
+ ),
+ ],
+} as Meta
+
+const largeContent = `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.
`
+
+export const Primary: StoryObj = {
+ args: {
+ maxLines: 6,
+ },
+ render: (args) => ({
+ template: `
+ ${largeContent}
+
`,
+ }),
+}
diff --git a/libs/ui/elements/src/lib/max-lines/max-lines.component.ts b/libs/ui/elements/src/lib/max-lines/max-lines.component.ts
new file mode 100644
index 0000000000..ff55be7b87
--- /dev/null
+++ b/libs/ui/elements/src/lib/max-lines/max-lines.component.ts
@@ -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])
+ }
+}
diff --git a/libs/ui/elements/src/lib/metadata-info/metadata-info.component.html b/libs/ui/elements/src/lib/metadata-info/metadata-info.component.html
index 49f224d172..d614a93be2 100644
--- a/libs/ui/elements/src/lib/metadata-info/metadata-info.component.html
+++ b/libs/ui/elements/src/lib/metadata-info/metadata-info.component.html
@@ -6,29 +6,31 @@
-
+
+
+
+
+
+ record.metadata.keywords
+
+
+ {{ keyword }}
+
+
+
+
-
-
- record.metadata.keywords
-
-
- {{ keyword }}
-
-
-