From 177068b825d868df1273cc234d7d1490ac9c36ae Mon Sep 17 00:00:00 2001 From: Jakob Engelbrecht Date: Mon, 15 May 2023 14:21:47 +0200 Subject: [PATCH] Enhancement: toolbar and header visual changes (#3041) Co-authored-by: mark-drastrup --- libs/core/src/scss/base/_functions.scss | 11 - libs/core/src/scss/base/_variables.scss | 1 + .../designsystem/page/src/page.component.html | 5 +- .../designsystem/page/src/page.component.scss | 133 ++- .../page/src/page.component.spec.ts | 993 ++++++++++++------ libs/designsystem/page/src/page.component.ts | 19 +- .../tab-navigation-item.component.scss | 3 - .../tab-navigation.component.html | 6 +- .../tab-navigation.component.scss | 55 +- .../src/element-css-custom-matchers.d.ts | 13 +- .../src/element-css-custom-matchers.ts | 27 +- libs/designsystem/testing/src/test-helper.ts | 4 +- 12 files changed, 833 insertions(+), 437 deletions(-) diff --git a/libs/core/src/scss/base/_functions.scss b/libs/core/src/scss/base/_functions.scss index cd12212eb9..0cb121a068 100644 --- a/libs/core/src/scss/base/_functions.scss +++ b/libs/core/src/scss/base/_functions.scss @@ -110,17 +110,6 @@ @return $classes; } -/// Remove the unit of a length -/// @param {Number} $number - Number to remove unit from -/// @return {Number} - Unitless number -/// Source: https://css-tricks.com/snippets/sass/strip-unit-function/ -@function strip-unit($number) { - @if meta.type-of($number) == 'number' and not math.is-unitless($number) { - @return math.div($number, $number * 0 + 1); - } - @return $number; -} - @mixin slotted($selectors...) { /* stylelint-disable-next-line selector-pseudo-element-no-unknown */ ::ng-deep > { diff --git a/libs/core/src/scss/base/_variables.scss b/libs/core/src/scss/base/_variables.scss index de8e03f905..ba80b451fc 100644 --- a/libs/core/src/scss/base/_variables.scss +++ b/libs/core/src/scss/base/_variables.scss @@ -5,6 +5,7 @@ SPACINGS ****************************************************************************/ $sizes: ( + xxxxxl: 72px, xxxxl: 64px, xxxl: 56px, xxl: 48px, diff --git a/libs/designsystem/page/src/page.component.html b/libs/designsystem/page/src/page.component.html index c57edef2a8..f1c058d8f0 100644 --- a/libs/designsystem/page/src/page.component.html +++ b/libs/designsystem/page/src/page.component.html @@ -1,5 +1,8 @@ - + =medium') { - --min-height: #{utils.size('xxxxl')}; + --min-height: #{utils.size('xxxxxl')}; } box-sizing: border-box; padding-inline: utils.size('xxxs'); - &.content-scrolled::before { - // Divider + /* + * Toolbar Divider & Shaded background + */ + + // Divider: + &::before { content: ''; position: absolute; left: 0; right: 0; bottom: 0; height: 1px; - z-index: 1; - background-color: utils.get-color('medium'); + } + + &::before, + &::part(background) { + transition: $toolbar-transition; + } + + // Only show divider + shaded bg on small screens when content is scrolled: + &.content-scrolled { + --background: #{$toolbar-shaded-background}; + + &::before, + &::part(background) { + transition-duration: $toolbar-transition-duration-in; + } + + &:not(.content-pinned)::before { + background-color: utils.get-color('medium'); + } + } + + // Always show divider + shaded bg on large screens: + @include utils.media('>=medium') { + --background: #{$toolbar-shaded-background}; + + // Divider: + &:not(.content-pinned)::before { + background-color: utils.get-color('medium'); + transition-duration: $toolbar-transition-duration-out; + } } /* @@ -287,31 +300,47 @@ ion-content { margin: 0 auto; } - &.content-pinned::before { - // Background + &::before, + &::after { + /* Background + Divider */ content: ''; position: absolute; - // Adjust for padding of ion-content to stretch divider to full width. + // Adjust for padding of ion-content to stretch divider to full width: left: calc(-1 * var(--padding-start)); right: calc(-1 * var(--padding-end)); bottom: 0; - top: 0; - z-index: 0; - background-color: var(--kirby-background-color); + transition: $toolbar-transition; } - &.content-pinned::after { - // Divider - content: ''; - position: absolute; + &::before { + /* Background */ + top: 0; - // Adjust for padding of ion-content to stretch divider to full width. - left: calc(-1 * var(--padding-start)); - right: calc(-1 * var(--padding-end)); - bottom: 0; + // Same bg color as ion-content to prevent see-through + // when scrolling the page behind the sticky content: + background-color: var(--background); + } + + &::after { + /* Divider */ height: 1px; - z-index: 1; - background-color: utils.get-color('medium'); + } + + &.content-pinned { + &::before, + &::after { + transition-duration: $toolbar-transition-duration-in; + } + + &::before { + /* Background - pinned */ + background-color: $toolbar-shaded-background; + } + + &::after { + /* Divider - pinned */ + background-color: utils.get-color('medium'); + } } } diff --git a/libs/designsystem/page/src/page.component.spec.ts b/libs/designsystem/page/src/page.component.spec.ts index d0a8b8784e..ee4f64b67f 100644 --- a/libs/designsystem/page/src/page.component.spec.ts +++ b/libs/designsystem/page/src/page.component.spec.ts @@ -10,7 +10,7 @@ import { WindowRef } from '@kirbydesign/designsystem/types'; import { TestHelper } from '@kirbydesign/designsystem/testing'; import { selectedTabClickEvent, TabsComponent } from '@kirbydesign/designsystem/tabs'; -const { size, fontWeight } = DesignTokenHelper; +const { size, fontWeight, getColor } = DesignTokenHelper; import { ModalNavigationService } from '@kirbydesign/designsystem/modal'; import { ButtonComponent } from '@kirbydesign/designsystem/button'; @@ -20,6 +20,7 @@ import { PageActionsDirective, PageComponent, PageContentComponent, + PageStickyContentDirective, PageSubtitleDirective, PageTitleDirective, PageToolbarTitleDirective, @@ -32,6 +33,7 @@ describe('PageComponent', () => { const firstOtherUrl = 'firstOther'; const secondOtherUrl = 'secondOther'; const firstOtherUrlWithQueryParams = 'firstOther?query=params'; + const shadedBackgroundColor = '#f3f3f3'; let spectator: SpectatorHost; let ionToolbar: HTMLElement; let ionContent: HTMLIonContentElement; @@ -81,6 +83,7 @@ describe('PageComponent', () => { PageSubtitleDirective, PageTitleDirective, PageToolbarTitleDirective, + PageStickyContentDirective, ], providers: [ { @@ -92,9 +95,18 @@ describe('PageComponent', () => { ], }); - beforeEach(async () => { - spectator = createHost( - ` + beforeAll(() => { + //Ensure css transitions run immediately: + const testStyles = window.document.createElement('style'); + testStyles.innerHTML = + '*, *::before, *::after, ::part(background) { transition-duration: 0ms !important; }'; + window.document.body.appendChild(testStyles); + }); + + describe('by default', () => { + beforeEach(async () => { + spectator = createHost( + ` @@ -102,436 +114,775 @@ describe('PageComponent', () => { ${dummyContent} ` - ); - modalNavigationService = spectator.inject(ModalNavigationService); - modalNavigationService.isModalRoute.and.returnValue(false); - router = spectator.inject(Router); - tabBar = spectator.inject(TabsComponent); - ionToolbar = spectator.queryHost('ion-toolbar'); - ionContent = spectator.queryHost('ion-content'); - await TestHelper.whenReady(ionToolbar); - await TestHelper.whenReady(ionContent); - }); - - describe('toolbar with dynamic height', () => { - it('should be correct height on ios-phone without top-safe-area', async () => { - ionToolbar.style.setProperty('--kirby-safe-area-top', '0px'); - await TestHelper.resizeTestWindow(TestHelper.screensize.phone); - - expect(ionToolbar).toHaveComputedStyle({ height: size('xxxl') }); + ); + modalNavigationService = spectator.inject(ModalNavigationService); + modalNavigationService.isModalRoute.and.returnValue(false); + router = spectator.inject(Router); + tabBar = spectator.inject(TabsComponent); + ionToolbar = spectator.queryHost('ion-toolbar'); + + ionContent = spectator.queryHost('ion-content'); + await TestHelper.whenReady(ionToolbar); + await TestHelper.whenReady(ionContent); }); - it('should be correct height on ios-phone with top-safe-area', async () => { - ionToolbar.style.setProperty('--kirby-safe-area-top', '33px'); - await TestHelper.resizeTestWindow(TestHelper.screensize.phone); - expect(ionToolbar).toHaveComputedStyle({ height: size('xxl') }); - }); - it('should be correct height on non-ios-phone', async () => { - await TestHelper.resizeTestWindow(TestHelper.screensize.phone); + describe('on phone', () => { + beforeAll(async () => { + await TestHelper.resizeTestWindow(TestHelper.screensize.phone); + }); - expect(ionToolbar).toHaveComputedStyle({ height: size('xxxl') }); - }); - it('should be correct height on tablet', async () => { - await TestHelper.resizeTestWindow(TestHelper.screensize.tablet); + afterAll(() => { + TestHelper.resetTestWindow(); + }); - expect(ionToolbar).toHaveComputedStyle({ height: size('xxxxl') }); - }); - it('should be correct height on desktop', async () => { - await TestHelper.resizeTestWindow(TestHelper.screensize.desktop); + describe('toolbar', () => { + describe('height', () => { + it('should be correct on ios-phone without top-safe-area', () => { + ionToolbar.style.setProperty('--kirby-safe-area-top', '0px'); + + expect(ionToolbar).toHaveComputedStyle({ height: size('xxxl') }); + }); + it('should be correct on ios-phone with top-safe-area', () => { + ionToolbar.style.setProperty('--kirby-safe-area-top', '33px'); + + expect(ionToolbar).toHaveComputedStyle({ height: size('xxxl') }); + }); + it('should be correct on non-ios-phone', () => { + expect(ionToolbar).toHaveComputedStyle({ height: size('xxxl') }); + }); + }); - expect(ionToolbar).toHaveComputedStyle({ height: size('xxxxl') }); + describe('divider and shaded background', () => { + describe('before scroll', () => { + it('should not render toolbar divider', () => { + expect(ionToolbar).toHaveComputedStyle( + { + 'background-color': 'rgba(0, 0, 0, 0)', + }, + ':before' + ); + }); + + it('should not render shaded toolbar background', () => { + const toolbarBackground = ionToolbar.shadowRoot.querySelector('.toolbar-background'); + expect(toolbarBackground).toHaveComputedStyle({ + 'background-color': getColor('background-color'), + }); + }); + }); + + describe('after scrolling page title above content top', () => { + beforeEach(async () => { + // Scroll page title above content top: + const pageTitle: HTMLElement = ionContent.querySelector('.page-title'); + const andThenSome = 10; + const verticalScrollAmount = + pageTitle.offsetTop + pageTitle.offsetHeight + andThenSome; + + await ionContent.scrollToPoint(0, verticalScrollAmount, 0); + await TestHelper.whenTrue(() => spectator.component.isContentScrolled); + }); + + it('should render toolbar divider', () => { + expect(ionToolbar).toHaveComputedStyle( + { + 'background-color': getColor('medium'), + }, + ':before' + ); + }); + + it('should render shaded toolbar background', () => { + const toolbarBackground = ionToolbar.shadowRoot.querySelector('.toolbar-background'); + expect(toolbarBackground).toHaveComputedStyle({ + 'background-color': shadedBackgroundColor, + }); + }); + }); + }); + }); }); - }); - describe('having static page action', () => { - it('should show the action in the toolbar when content not scrolled', () => { - const staticPageActionButton = ionToolbar.querySelector( - 'ion-buttons[slot="primary"] button[kirby-button]' - ); + describe('on tablet', () => { + beforeAll(async () => { + await TestHelper.resizeTestWindow(TestHelper.screensize.tablet); + }); + + afterAll(() => { + TestHelper.resetTestWindow(); + }); - expect(staticPageActionButton).toBeTruthy(); + describe('toolbar', () => { + describe('height', () => { + it('should be correct on tablet', () => { + expect(ionToolbar).toHaveComputedStyle({ height: size('xxxxxl') }); + }); + }); + }); }); - it('should show the action in the toolbar when content scrolled', async () => { - ionContent.style.height = '200px'; - await ionContent.scrollToPoint(0, 16, 0); - spectator.detectChanges(); - await TestHelper.whenTrue(() => spectator.component['isContentScrolled']); - spectator.detectChanges(); + describe('on desktop', () => { + beforeAll(async () => { + await TestHelper.resizeTestWindow(TestHelper.screensize.desktop); + }); - expect(ionToolbar).toHaveClass('content-scrolled'); + afterAll(() => { + TestHelper.resetTestWindow(); + }); - const staticPageActionButton = ionToolbar.querySelector( - 'ion-buttons[slot="primary"] button[kirby-button]' - ); + describe('toolbar', () => { + describe('height', () => { + it('should be correct on desktop', () => { + expect(ionToolbar).toHaveComputedStyle({ height: size('xxxxxl') }); + }); + }); - expect(staticPageActionButton).toBeTruthy(); + it('should render toolbar divider by default', () => { + expect(ionToolbar).toHaveComputedStyle( + { + 'background-color': getColor('medium'), + }, + ':before' + ); + }); + + it('should render shaded toolbar background by default', () => { + const toolbarBackground = ionToolbar.shadowRoot.querySelector('.toolbar-background'); + expect(toolbarBackground).toHaveComputedStyle({ + 'background-color': shadedBackgroundColor, + }); + }); + }); }); - }); - describe('having a title and subtitle', () => { - it('should have the configured title in the toolbar-title', () => { - const toolbarTitle = ionToolbar.querySelector('ion-title .toolbar-title'); + describe('having static page action', () => { + it('should show the action in the toolbar when content not scrolled', () => { + const staticPageActionButton = ionToolbar.querySelector( + 'ion-buttons[slot="primary"] button[kirby-button]' + ); - expect(toolbarTitle).toHaveText(titleText); - }); + expect(staticPageActionButton).toBeTruthy(); + }); - it('should render the toolbar-title with the correct font-weight', () => { - const toolbarTitle = ionToolbar.querySelector('ion-title .toolbar-title'); + it('should show the action in the toolbar when content scrolled', async () => { + const verticalScrollAmount = 10; + await ionContent.scrollToPoint(0, verticalScrollAmount, 0); + await TestHelper.whenTrue(() => spectator.component.isContentScrolled); - expect(toolbarTitle).toHaveComputedStyle({ - 'font-weight': fontWeight('bold'), + const staticPageActionButton = ionToolbar.querySelector( + 'ion-buttons[slot="primary"] button[kirby-button]' + ); + + expect(staticPageActionButton).toBeTruthy(); }); }); - it('should have the configured title', () => { - const pageTitleHeading = ionContent.querySelector('.page-title > h1'); + describe('having a title and subtitle', () => { + it('should have the configured title in the toolbar-title', () => { + const toolbarTitle = ionToolbar.querySelector('ion-title .toolbar-title'); - expect(spectator.component.title).toEqual(titleText); - expect(pageTitleHeading).toHaveText(titleText, true); - }); + expect(toolbarTitle).toHaveText(titleText); + }); - it('should render title with correct margin and padding', () => { - const pageTitle = ionContent.querySelector('.page-title'); - const pageTitleHeading = pageTitle.querySelector(':scope > h1'); - - expect(pageTitle).toHaveComputedStyle({ - 'margin-left': '0px', - 'margin-right': '0px', - 'margin-top': '0px', - 'margin-bottom': '0px', - 'padding-left': '0px', - 'padding-right': '0px', - 'padding-top': '0px', - 'padding-bottom': '0px', + it('should render the toolbar-title with the correct font-weight', () => { + const toolbarTitle = ionToolbar.querySelector('ion-title .toolbar-title'); + + expect(toolbarTitle).toHaveComputedStyle({ + 'font-weight': fontWeight('bold'), + }); }); - expect(pageTitleHeading).toHaveComputedStyle({ - 'margin-left': '0px', - 'margin-right': '0px', - 'margin-top': '0px', - 'margin-bottom': '0px', - 'padding-left': '0px', - 'padding-right': '0px', - 'padding-top': '0px', - 'padding-bottom': '0px', + + it('should have the configured title', () => { + const pageTitleHeading = ionContent.querySelector('.page-title > h1'); + + expect(spectator.component.title).toEqual(titleText); + expect(pageTitleHeading).toHaveText(titleText, true); }); - }); - it('should have the configured subtitle', () => { - const pageSubtitle = ionContent.querySelector('.page-subtitle'); + it('should render title with correct margin and padding', () => { + const pageTitle = ionContent.querySelector('.page-title'); + const pageTitleHeading = pageTitle.querySelector(':scope > h1'); + + expect(pageTitle).toHaveComputedStyle({ + 'margin-left': '0px', + 'margin-right': '0px', + 'margin-top': '0px', + 'margin-bottom': '0px', + 'padding-left': '0px', + 'padding-right': '0px', + 'padding-top': '0px', + 'padding-bottom': '0px', + }); + expect(pageTitleHeading).toHaveComputedStyle({ + 'margin-left': '0px', + 'margin-right': '0px', + 'margin-top': '0px', + 'margin-bottom': '0px', + 'padding-left': '0px', + 'padding-right': '0px', + 'padding-top': '0px', + 'padding-bottom': '0px', + }); + }); - expect(spectator.component.subtitle).toEqual(subtitleText); - expect(pageSubtitle).toHaveText(subtitleText, true); - }); + it('should have the configured subtitle', () => { + const pageSubtitle = ionContent.querySelector('.page-subtitle'); - it('should render subitle with correct margin and padding', () => { - const pageSubtitle = ionContent.querySelector('.page-subtitle'); + expect(spectator.component.subtitle).toEqual(subtitleText); + expect(pageSubtitle).toHaveText(subtitleText, true); + }); - expect(pageSubtitle).toHaveComputedStyle({ - 'margin-left': '0px', - 'margin-right': '0px', - 'margin-top': size('xxs'), - 'margin-bottom': '0px', - 'padding-left': '0px', - 'padding-right': '0px', - 'padding-top': '0px', - 'padding-bottom': '0px', + it('should render subtitle with correct margin and padding', () => { + const pageSubtitle = ionContent.querySelector('.page-subtitle'); + + expect(pageSubtitle).toHaveComputedStyle({ + 'margin-left': '0px', + 'margin-right': '0px', + 'margin-top': size('xxs'), + 'margin-bottom': '0px', + 'padding-left': '0px', + 'padding-right': '0px', + 'padding-top': '0px', + 'padding-bottom': '0px', + }); }); }); - }); - describe('having a title and subtitle', () => { - it('should have the configured title', async () => { - await TestHelper.whenReady(ionContent); - const pageTitleHeading = ionContent.querySelector('.page-title > h1'); - - expect(spectator.component.title).toEqual(titleText); - expect(pageTitleHeading).toHaveText(titleText, true); - }); + describe('having a title and subtitle', () => { + it('should have the configured title', async () => { + await TestHelper.whenReady(ionContent); + const pageTitleHeading = ionContent.querySelector('.page-title > h1'); - it('should render title with correct margin and padding', async () => { - await TestHelper.whenReady(ionContent); - const pageTitle = ionContent.querySelector('.page-title'); - const pageTitleHeading = pageTitle.querySelector(':scope > h1'); - - expect(pageTitle).toHaveComputedStyle({ - 'margin-left': '0px', - 'margin-right': '0px', - 'margin-top': '0px', - 'margin-bottom': '0px', - 'padding-left': '0px', - 'padding-right': '0px', - 'padding-top': '0px', - 'padding-bottom': '0px', + expect(spectator.component.title).toEqual(titleText); + expect(pageTitleHeading).toHaveText(titleText, true); }); - expect(pageTitleHeading).toHaveComputedStyle({ - 'margin-left': '0px', - 'margin-right': '0px', - 'margin-top': '0px', - 'margin-bottom': '0px', - 'padding-left': '0px', - 'padding-right': '0px', - 'padding-top': '0px', - 'padding-bottom': '0px', + + it('should render title with correct margin and padding', async () => { + await TestHelper.whenReady(ionContent); + const pageTitle = ionContent.querySelector('.page-title'); + const pageTitleHeading = pageTitle.querySelector(':scope > h1'); + + expect(pageTitle).toHaveComputedStyle({ + 'margin-left': '0px', + 'margin-right': '0px', + 'margin-top': '0px', + 'margin-bottom': '0px', + 'padding-left': '0px', + 'padding-right': '0px', + 'padding-top': '0px', + 'padding-bottom': '0px', + }); + expect(pageTitleHeading).toHaveComputedStyle({ + 'margin-left': '0px', + 'margin-right': '0px', + 'margin-top': '0px', + 'margin-bottom': '0px', + 'padding-left': '0px', + 'padding-right': '0px', + 'padding-top': '0px', + 'padding-bottom': '0px', + }); }); - }); - it('should have the configured subtitle', async () => { - await TestHelper.whenReady(ionContent); - const pageSubtitle = ionContent.querySelector('.page-subtitle'); + it('should have the configured subtitle', async () => { + await TestHelper.whenReady(ionContent); + const pageSubtitle = ionContent.querySelector('.page-subtitle'); - expect(spectator.component.subtitle).toEqual(subtitleText); - expect(pageSubtitle).toHaveText(subtitleText, true); + expect(spectator.component.subtitle).toEqual(subtitleText); + expect(pageSubtitle).toHaveText(subtitleText, true); + }); + + it('should render subtitle with correct margin and padding', async () => { + await TestHelper.whenReady(ionContent); + const pageSubtitle = ionContent.querySelector('.page-subtitle'); + + expect(pageSubtitle).toHaveComputedStyle({ + 'margin-left': '0px', + 'margin-right': '0px', + 'margin-top': size('xxs'), + 'margin-bottom': '0px', + 'padding-left': '0px', + 'padding-right': '0px', + 'padding-top': '0px', + 'padding-bottom': '0px', + }); + }); }); - it('should render subitle with correct margin and padding', async () => { - await TestHelper.whenReady(ionContent); - const pageSubtitle = ionContent.querySelector('.page-subtitle'); - - expect(pageSubtitle).toHaveComputedStyle({ - 'margin-left': '0px', - 'margin-right': '0px', - 'margin-top': size('xxs'), - 'margin-bottom': '0px', - 'padding-left': '0px', - 'padding-right': '0px', + it('should render toolbar with correct padding', async () => { + await TestHelper.whenReady(ionToolbar); + const toolbarContainer = ionToolbar.shadowRoot.querySelector('.toolbar-container'); + expect(toolbarContainer).toBeTruthy(); + expect(toolbarContainer).toHaveComputedStyle({ + 'padding-left': size('xxxs'), + 'padding-right': size('xxxs'), 'padding-top': '0px', 'padding-bottom': '0px', }); }); - }); - it('should render toolbar with correct padding', async () => { - await TestHelper.whenReady(ionToolbar); - const toolbarContainer = ionToolbar.shadowRoot.querySelector('.toolbar-container'); - expect(toolbarContainer).toBeTruthy(); - expect(toolbarContainer).toHaveComputedStyle({ - 'padding-left': size('xxxs'), - 'padding-right': size('xxxs'), - 'padding-top': '0px', - 'padding-bottom': '0px', + it('should render back button with correct size', async () => { + await TestHelper.whenReady(ionToolbar); + const ionBackButton = spectator.queryHost('ion-toolbar ion-buttons ion-back-button'); + expect(ionBackButton).toHaveComputedStyle({ + width: size('xl'), + height: size('xl'), + }); }); - }); - it('should render back button with correct size', async () => { - await TestHelper.whenReady(ionToolbar); - const ionBackButton = spectator.queryHost('ion-toolbar ion-buttons ion-back-button'); - expect(ionBackButton).toHaveComputedStyle({ - width: size('xl'), - height: size('xl'), - }); - }); + it('should hide tab bar when tabBarBottomHidden is true', fakeAsync(() => { + expect(tabBar.tabBarBottomHidden).toBe(false); - it('should hide tab bar when tabBarBottomHidden is true', fakeAsync(() => { - expect(tabBar.tabBarBottomHidden).toBe(false); + spectator.setInput('tabBarBottomHidden', true); + spectator.detectChanges(); + tick(); - spectator.setInput('tabBarBottomHidden', true); - spectator.detectChanges(); - tick(); + expect(tabBar.tabBarBottomHidden).toBe(true); + })); - expect(tabBar.tabBarBottomHidden).toBe(true); - })); + it('should show tab bar when tabBarBottomHidden is false', fakeAsync(() => { + // hide tab bar + spectator.setInput('tabBarBottomHidden', true); + spectator.detectChanges(); + tick(); + expect(tabBar.tabBarBottomHidden).toBe(true); - it('should show tab bar when tabBarBottomHidden is false', fakeAsync(() => { - // hide tab bar - spectator.setInput('tabBarBottomHidden', true); - spectator.detectChanges(); - tick(); - expect(tabBar.tabBarBottomHidden).toBe(true); + // show tab bar + spectator.setInput('tabBarBottomHidden', false); + spectator.detectChanges(); + tick(); - // show tab bar - spectator.setInput('tabBarBottomHidden', false); - spectator.detectChanges(); - tick(); + expect(tabBar.tabBarBottomHidden).toBe(false); + })); - expect(tabBar.tabBarBottomHidden).toBe(false); - })); + it('should show tab bar when tabBarBottomHidden is true on leave', () => { + spectator.setInput('tabBarBottomHidden', true); - it('should show tab bar when tabBarBottomHidden is true on leave', () => { - spectator.setInput('tabBarBottomHidden', true); + navigateToUrl(firstOtherUrl); - navigateToUrl(firstOtherUrl); + expect(tabBar.tabBarBottomHidden).toBe(false); + }); - expect(tabBar.tabBarBottomHidden).toBe(false); - }); + describe('with enter and leave event binding', () => { + let enterEventHandler: jasmine.Spy; + let leaveEventHandler: jasmine.Spy; - describe('with enter and leave event binding', () => { - let enterEventHandler: jasmine.Spy; - let leaveEventHandler: jasmine.Spy; + beforeEach(() => { + enterEventHandler = jasmine.createSpy(); + leaveEventHandler = jasmine.createSpy(); + spectator.output('enter').subscribe(enterEventHandler); + spectator.output('leave').subscribe(leaveEventHandler); + }); - beforeEach(() => { - enterEventHandler = jasmine.createSpy(); - leaveEventHandler = jasmine.createSpy(); - spectator.output('enter').subscribe(enterEventHandler); - spectator.output('leave').subscribe(leaveEventHandler); - }); + it('should emit the correct event(s) when navigating navigating to the page', () => { + navigateToUrl(firstOtherUrl); + enterEventHandler.calls.reset(); + leaveEventHandler.calls.reset(); - it('should emit the correct event(s) when navigating navigating to the page', () => { - navigateToUrl(firstOtherUrl); - enterEventHandler.calls.reset(); - leaveEventHandler.calls.reset(); + navigateUrls([secondOtherUrl, pageUrl]); - navigateUrls([secondOtherUrl, pageUrl]); + expect(enterEventHandler).toHaveBeenCalledTimes(1); + expect(leaveEventHandler).toHaveBeenCalledTimes(0); + }); - expect(enterEventHandler).toHaveBeenCalledTimes(1); - expect(leaveEventHandler).toHaveBeenCalledTimes(0); - }); + it('should emit the correct event(s) when navigating away from the page', () => { + navigateToUrl(pageUrl); + enterEventHandler.calls.reset(); + leaveEventHandler.calls.reset(); - it('should emit the correct event(s) when navigating away from the page', () => { - navigateToUrl(pageUrl); - enterEventHandler.calls.reset(); - leaveEventHandler.calls.reset(); + navigateUrls([firstOtherUrl, secondOtherUrl]); - navigateUrls([firstOtherUrl, secondOtherUrl]); + expect(enterEventHandler).toHaveBeenCalledTimes(0); + expect(leaveEventHandler).toHaveBeenCalledTimes(1); + }); - expect(enterEventHandler).toHaveBeenCalledTimes(0); - expect(leaveEventHandler).toHaveBeenCalledTimes(1); - }); + it('should emit the correct event(s) when navigating away from the page and back again', () => { + navigateToUrl(pageUrl); + enterEventHandler.calls.reset(); + leaveEventHandler.calls.reset(); - it('should emit the correct event(s) when navigating away from the page and back again', () => { - navigateToUrl(pageUrl); - enterEventHandler.calls.reset(); - leaveEventHandler.calls.reset(); + navigateUrls([firstOtherUrl, secondOtherUrl, pageUrl]); - navigateUrls([firstOtherUrl, secondOtherUrl, pageUrl]); + expect(enterEventHandler).toHaveBeenCalledTimes(1); + expect(leaveEventHandler).toHaveBeenCalledTimes(1); + }); - expect(enterEventHandler).toHaveBeenCalledTimes(1); - expect(leaveEventHandler).toHaveBeenCalledTimes(1); - }); + it('should emit the correct event(s) when navigating to the page and away again', () => { + navigateToUrl(secondOtherUrl); + enterEventHandler.calls.reset(); + leaveEventHandler.calls.reset(); - it('should emit the correct event(s) when navigating to the page and away again', () => { - navigateToUrl(secondOtherUrl); - enterEventHandler.calls.reset(); - leaveEventHandler.calls.reset(); + navigateUrls([firstOtherUrl, secondOtherUrl, pageUrl, firstOtherUrl, secondOtherUrl]); - navigateUrls([firstOtherUrl, secondOtherUrl, pageUrl, firstOtherUrl, secondOtherUrl]); + expect(enterEventHandler).toHaveBeenCalledTimes(1); + expect(leaveEventHandler).toHaveBeenCalledTimes(1); + }); - expect(enterEventHandler).toHaveBeenCalledTimes(1); - expect(leaveEventHandler).toHaveBeenCalledTimes(1); + it('should not emit event(s) when changing query params', () => { + navigateToUrl(firstOtherUrl); + enterEventHandler.calls.reset(); + leaveEventHandler.calls.reset(); + + navigateToUrl(firstOtherUrlWithQueryParams); + + expect(enterEventHandler).toHaveBeenCalledTimes(0); + expect(leaveEventHandler).toHaveBeenCalledTimes(0); + }); }); - it('should not emit event(s) when changing query params', () => { - navigateToUrl(firstOtherUrl); - enterEventHandler.calls.reset(); - leaveEventHandler.calls.reset(); + describe('with a back-button', () => { + let ionBackButton; + + beforeEach(() => { + ionBackButton = spectator.queryHost('ion-toolbar ion-buttons ion-back-button'); + }); - navigateToUrl(firstOtherUrlWithQueryParams); + it('should call the default click handler if no back-button-click observer is provided', () => { + const defaultHandler = jasmine.createSpy(); + ionBackButton.onclick = defaultHandler; - expect(enterEventHandler).toHaveBeenCalledTimes(0); - expect(leaveEventHandler).toHaveBeenCalledTimes(0); + spectator.click(ionBackButton); + + expect(defaultHandler).toHaveBeenCalledTimes(1); + }); + + it('should emit an event on click if a back-button-click observer is provided', () => { + const subscriber = jasmine.createSpy(); + spectator.output('backButtonClick').subscribe(subscriber); + + spectator.click(ionBackButton); + + expect(subscriber).toHaveBeenCalledTimes(1); + }); }); - }); - describe('with a back-button', () => { - let ionBackButton; + describe('pull-to-refresh', () => { + it('should be available when "refresh" is subscribed to', () => { + spectator.output('refresh').subscribe(() => {}); + spectator.detectComponentChanges(); + expect(spectator.query(IonRefresher)).not.toBeNull(); + }); - beforeEach(() => { - ionBackButton = spectator.queryHost('ion-toolbar ion-buttons ion-back-button'); + it('should not be available when "refresh" is not subscribed to', () => { + expect(spectator.query(IonRefresher)).toBeNull(); + }); }); - it('should call the default click handler if no back-button-click observer is provided', () => { - const defaultHandler = jasmine.createSpy(); - ionBackButton.onclick = defaultHandler; + describe('with maxWidth is defined', () => { + it('should apply the correct content width', async () => { + await TestHelper.whenReady(ionContent); + const contentInner = ionContent.querySelector('.content-inner'); + expect(contentInner).toHaveComputedStyle({ + 'max-width': '720px', + }); + }); - spectator.click(ionBackButton); + describe('and is set to standard', () => { + beforeEach(() => { + spectator.component.maxWidth = 'standard'; + spectator.detectChanges(); + }); - expect(defaultHandler).toHaveBeenCalledTimes(1); - }); + it('should apply correct content width', async () => { + await TestHelper.whenReady(ionContent); + const contentInner = ionContent.querySelector('.content-inner'); + expect(contentInner).toHaveComputedStyle({ + 'max-width': '792px', + }); + }); + }); - it('should emit an event on click if a back-button-click observer is provided', () => { - const subscriber = jasmine.createSpy(); - spectator.output('backButtonClick').subscribe(subscriber); + describe('and is set to optimized', () => { + beforeEach(() => { + spectator.component.maxWidth = 'optimized'; + spectator.detectChanges(); + }); - spectator.click(ionBackButton); + it('should apply correct content width', async () => { + await TestHelper.whenReady(ionContent); + const contentInner = ionContent.querySelector('.content-inner'); + expect(contentInner).toHaveComputedStyle({ + 'max-width': '1092px', + }); + }); + }); - expect(subscriber).toHaveBeenCalledTimes(1); + describe('and is set to full', () => { + beforeEach(() => { + spectator.component.maxWidth = 'full'; + spectator.detectChanges(); + }); + + it('should apply correct content width', async () => { + await TestHelper.whenReady(ionContent); + const contentInner = ionContent.querySelector('.content-inner'); + expect(contentInner).toHaveComputedStyle({ + 'max-width': '100%', + }); + }); + }); }); - }); - describe('pull-to-refresh', () => { - it('should be available when "refresh" is subscribed to', () => { - spectator.output('refresh').subscribe(() => {}); - spectator.detectComponentChanges(); - expect(spectator.query(IonRefresher)).not.toBeNull(); + it('should scroll to top when tab is clicked', () => { + const scrollToTopSpy = jasmine.createSpy(); + spectator.component['content'].scrollToTop = scrollToTopSpy; + + window.dispatchEvent(new Event(selectedTabClickEvent)); + + expect(scrollToTopSpy).toHaveBeenCalledTimes(1); }); - it('should not be available when "refresh" is not subscribed to', () => { - expect(spectator.query(IonRefresher)).toBeNull(); + const navigateUrls = (urls: string[]) => { + urls.forEach((url: string) => navigateToUrl(url)); + }; + + const navigateToUrl = fakeAsync((url: string) => { + router.navigateByUrl(url); + tick(); }); }); - describe('with maxWidth is defined', () => { - it('should apply the correct content width', async () => { + describe('with sticky content', () => { + let ionScrollElement: HTMLElement; + let stickyContentContainer: HTMLElement; + + beforeEach(async () => { + spectator = createHost( + ` +
+ Sticky content +
+ + ${dummyContent} + +
` + ); + modalNavigationService = spectator.inject(ModalNavigationService); + modalNavigationService.isModalRoute.and.returnValue(false); + ionToolbar = spectator.queryHost('ion-toolbar'); + ionContent = spectator.queryHost('ion-content'); + await TestHelper.whenReady(ionToolbar); await TestHelper.whenReady(ionContent); - const contentInner = ionContent.querySelector('.content-inner'); - expect(contentInner).toHaveComputedStyle({ - 'max-width': '720px', + ionScrollElement = await ionContent.getScrollElement(); + stickyContentContainer = ionContent.querySelector('.sticky-content-container'); + + // Ensure content has height: + ionContent.style.height = '200px'; + + // Wait for sticky content intersection observer: + await TestHelper.whenTrue(() => !spectator.component.isStickyContentPinned); + expect(ionToolbar).not.toHaveClass('content-pinned'); + }); + + describe('by default', () => { + it('should render sticky content', () => { + expect(stickyContentContainer).toBeDefined(); + }); + + it('should render sticky content with correct background color', () => { + expect(stickyContentContainer).toHaveComputedStyle( + { + 'background-color': getColor('background-color'), + }, + ':before' + ); + }); + + it('should not render sticky content divider', () => { + expect(stickyContentContainer).toHaveComputedStyle( + { + 'background-color': 'rgba(0, 0, 0, 0)', + }, + ':after' + ); }); }); - describe('and is set to standard', () => { - beforeEach(() => { - spectator.component.maxWidth = 'standard'; - spectator.detectChanges(); + describe('on phone', () => { + beforeAll(async () => { + await TestHelper.resizeTestWindow(TestHelper.screensize.phone); }); - it('should apply correct content width', async () => { - await TestHelper.whenReady(ionContent); - const contentInner = ionContent.querySelector('.content-inner'); - expect(contentInner).toHaveComputedStyle({ - 'max-width': '792px', + afterAll(() => { + TestHelper.resetTestWindow(); + }); + + describe('before scroll', () => { + it('should not render toolbar divider', () => { + expect(ionToolbar).toHaveComputedStyle( + { + 'background-color': 'rgba(0, 0, 0, 0)', + }, + ':before' + ); + }); + + it('should not render shaded toolbar background', () => { + const toolbarBackground = ionToolbar.shadowRoot.querySelector('.toolbar-background'); + expect(toolbarBackground).toHaveComputedStyle({ + 'background-color': getColor('background-color'), + }); }); }); - }); - describe('and is set to optimized', () => { - beforeEach(() => { - spectator.component.maxWidth = 'optimized'; - spectator.detectChanges(); + describe('after scrolling page title above content top', () => { + beforeEach(async () => { + // Scroll page title above content top: + const pageTitle: HTMLElement = ionContent.querySelector('.page-title'); + const andThenSome = 10; + const verticalScrollAmount = pageTitle.offsetTop + pageTitle.offsetHeight + andThenSome; + + await ionContent.scrollToPoint(0, verticalScrollAmount, 0); + await TestHelper.whenTrue(() => spectator.component.isContentScrolled); + }); + + it('should render toolbar divider', () => { + expect(ionToolbar).toHaveComputedStyle( + { + 'background-color': getColor('medium'), + }, + ':before' + ); + }); + + it('should render shaded toolbar background', () => { + const toolbarBackground = ionToolbar.shadowRoot.querySelector('.toolbar-background'); + expect(toolbarBackground).toHaveComputedStyle({ + 'background-color': shadedBackgroundColor, + }); + }); }); - it('should apply correct content width', async () => { - await TestHelper.whenReady(ionContent); - const contentInner = ionContent.querySelector('.content-inner'); - expect(contentInner).toHaveComputedStyle({ - 'max-width': '1092px', + describe('after scrolling sticky content above content top', () => { + beforeEach(async () => { + // Scroll sticky content above content top: + const andThenSome = 10; + const verticalScrollAmount = stickyContentContainer.offsetTop + andThenSome; + await ionContent.scrollToPoint(0, verticalScrollAmount, 0); + await TestHelper.whenTrue(() => spectator.component.isContentScrolled); + await TestHelper.whenTrue(() => spectator.component.isStickyContentPinned); + }); + + it('should render sticky content with correct background color', () => { + expect(stickyContentContainer).toHaveComputedStyle( + { + 'background-color': shadedBackgroundColor, + height: `${stickyContentContainer.offsetHeight}px`, + width: `${ionScrollElement.clientWidth}px`, + }, + ':before' + ); + }); + + it('should render sticky content divider', () => { + expect(stickyContentContainer).toHaveComputedStyle( + { + 'background-color': getColor('medium'), + content: '""', + height: '1px', + width: `${ionScrollElement.clientWidth}px`, + }, + ':after' + ); + }); + + it('should not render toolbar divider', () => { + expect(ionToolbar).toHaveComputedStyle( + { + 'background-color': 'rgba(0, 0, 0, 0)', + }, + ':before' + ); + }); + + it('should render shaded toolbar background', () => { + const toolbarBackground = ionToolbar.shadowRoot.querySelector('.toolbar-background'); + expect(toolbarBackground).toHaveComputedStyle({ + 'background-color': shadedBackgroundColor, + }); }); }); }); - describe('and is set to full', () => { - beforeEach(() => { - spectator.component.maxWidth = 'full'; - spectator.detectChanges(); + describe('on desktop', () => { + beforeAll(async () => { + await TestHelper.resizeTestWindow(TestHelper.screensize.desktop); }); - it('should apply correct content width', async () => { - await TestHelper.whenReady(ionContent); - const contentInner = ionContent.querySelector('.content-inner'); - expect(contentInner).toHaveComputedStyle({ - 'max-width': '100%', + afterAll(() => { + TestHelper.resetTestWindow(); + }); + + describe('before scroll', () => { + it('should render toolbar divider by default', () => { + expect(ionToolbar).toHaveComputedStyle( + { + 'background-color': getColor('medium'), + content: '""', + height: '1px', + width: `${ionToolbar.offsetWidth}px`, + }, + ':before' + ); + }); + + it('should render shaded toolbar background by default', () => { + const toolbarBackground = ionToolbar.shadowRoot.querySelector('.toolbar-background'); + expect(toolbarBackground).toHaveComputedStyle({ + 'background-color': shadedBackgroundColor, + }); }); }); - }); - }); - describe('tab navigation', () => { - it('should scroll to top when tab is clicked', () => { - const scrollToTopSpy = jasmine.createSpy(); - (spectator.component as any).content.scrollToTop = scrollToTopSpy; + describe('after scrolling sticky content above content top', () => { + beforeEach(async () => { + // Scroll sticky content above content top: + const andThenSome = 10; + const verticalScrollAmount = stickyContentContainer.offsetTop + andThenSome; + await ionContent.scrollToPoint(0, verticalScrollAmount, 0); + await TestHelper.whenTrue(() => spectator.component.isContentScrolled); + await TestHelper.whenTrue(() => spectator.component.isStickyContentPinned); + }); - window.dispatchEvent(new Event(selectedTabClickEvent)); + it('should render sticky content with correct background color', () => { + expect(stickyContentContainer).toHaveComputedStyle( + { + 'background-color': shadedBackgroundColor, + height: `${stickyContentContainer.offsetHeight}px`, + width: `${ionScrollElement.clientWidth}px`, + }, + ':before' + ); + }); - expect(scrollToTopSpy).toHaveBeenCalledTimes(1); - }); - }); + it('should render sticky content divider', () => { + expect(stickyContentContainer).toHaveComputedStyle( + { + 'background-color': getColor('medium'), + content: '""', + height: '1px', + width: `${ionScrollElement.clientWidth}px`, + }, + ':after' + ); + }); - const navigateUrls = (urls: string[]) => { - urls.forEach((url: string) => navigateToUrl(url)); - }; + it('should not render toolbar divider', () => { + expect(ionToolbar).toHaveComputedStyle( + { + 'background-color': 'rgba(0, 0, 0, 0)', + }, + ':before' + ); + }); - const navigateToUrl = fakeAsync((url: string) => { - router.navigateByUrl(url); - tick(); + it('should render shaded toolbar background', () => { + const toolbarBackground = ionToolbar.shadowRoot.querySelector('.toolbar-background'); + expect(toolbarBackground).toHaveComputedStyle({ + 'background-color': shadedBackgroundColor, + }); + }); + }); + }); }); }); diff --git a/libs/designsystem/page/src/page.component.ts b/libs/designsystem/page/src/page.component.ts index 0cea6db511..35e38fdbcd 100644 --- a/libs/designsystem/page/src/page.component.ts +++ b/libs/designsystem/page/src/page.component.ts @@ -35,6 +35,7 @@ import { IonFooter, IonHeader, IonRouterOutlet, + IonToolbar, NavController, } from '@ionic/angular'; import { ScrollDetail } from '@ionic/core'; @@ -241,6 +242,8 @@ export class PageComponent ionHeaderElement: ElementRef; @ViewChild(IonFooter, { static: true, read: ElementRef }) private ionFooterElement: ElementRef; + @ViewChild(IonToolbar, { static: true, read: ElementRef }) + private ionToolbarElement: ElementRef; @ViewChild(IonBackButtonDelegate, { static: false }) private backButtonDelegate: IonBackButtonDelegate; @@ -350,6 +353,7 @@ export class PageComponent ngOnInit(): void { this.removeWrapper(); + this.setToolbarBackgroundPart(); const actionGroupConfig: ActionGroupConfig = { isResizable: false, @@ -466,6 +470,18 @@ export class PageComponent this.renderer.appendChild(parent, this.ionFooterElement.nativeElement); } + private setToolbarBackgroundPart() { + // Ensure ion-toolbar custom element has been defined (primarily when testing, but doesn't hurt): + customElements.whenDefined(this.ionToolbarElement.nativeElement.localName).then(() => { + this.ionToolbarElement.nativeElement.componentOnReady().then((toolbar) => { + const toolbarBackground = toolbar.shadowRoot.querySelector('.toolbar-background'); + if (toolbarBackground) { + this.renderer.setAttribute(toolbarBackground, 'part', 'background'); + } + }); + }); + } + private onEnter() { if (this.isActive) return; this.isActive = true; @@ -716,8 +732,7 @@ export class PageComponent private createStickyContentIntersectionObserver() { const options: IntersectionObserverInit = { - // TODO: Should sticky content also use ion-content as root? - // root: this.ionContentElement.nativeElement, + root: this.ionContentElement.nativeElement, threshold: 1, }; diff --git a/libs/designsystem/tab-navigation/src/tab-navigation-item/tab-navigation-item.component.scss b/libs/designsystem/tab-navigation/src/tab-navigation-item/tab-navigation-item.component.scss index 198747ed26..be21073751 100644 --- a/libs/designsystem/tab-navigation/src/tab-navigation-item/tab-navigation-item.component.scss +++ b/libs/designsystem/tab-navigation/src/tab-navigation-item/tab-navigation-item.component.scss @@ -2,9 +2,7 @@ $tab-item-text-max-width: 100px; $bottom-border-height: 1px; -$border-color-standard: utils.get-color('medium'); $border-color-selected: utils.get-color('dark'); -$divider-max-width-breakpoint: utils.$page-content-max-width + (utils.size('s') * 2); @mixin button-reset { background: transparent; @@ -33,7 +31,6 @@ $divider-max-width-breakpoint: utils.$page-content-max-width + (utils.size('s') button[role='tab'] { @include button-reset; - background-color: utils.get-color('background-color'); color: utils.get-color('black'); box-sizing: border-box; // Ensure border is not added to button height padding: utils.size('s') utils.size('m'); diff --git a/libs/designsystem/tab-navigation/src/tab-navigation/tab-navigation.component.html b/libs/designsystem/tab-navigation/src/tab-navigation/tab-navigation.component.html index 2fffd393ee..379ba412dd 100644 --- a/libs/designsystem/tab-navigation/src/tab-navigation/tab-navigation.component.html +++ b/libs/designsystem/tab-navigation/src/tab-navigation/tab-navigation.component.html @@ -1,5 +1,3 @@ -
-
- -
+
+
diff --git a/libs/designsystem/tab-navigation/src/tab-navigation/tab-navigation.component.scss b/libs/designsystem/tab-navigation/src/tab-navigation/tab-navigation.component.scss index 96b96cac2c..0345e378bc 100644 --- a/libs/designsystem/tab-navigation/src/tab-navigation/tab-navigation.component.scss +++ b/libs/designsystem/tab-navigation/src/tab-navigation/tab-navigation.component.scss @@ -1,11 +1,35 @@ @use '@kirbydesign/core/src/scss/utils'; -$tab-item-text-max-width: 100px; $bottom-border-height: 1px; $border-color-standard: utils.get-color('medium'); -$border-color-selected: utils.get-color('dark'); $divider-max-width-breakpoint: utils.$page-content-max-width + (utils.size('s') * 2); +:host { + display: block; + position: relative; + + @include utils.media(' { - toHaveComputedStyle(styles: { - [cssProperty: string]: - | string - | import('@kirbydesign/designsystem/helpers').ThemeColorDefinition; - }): boolean; + toHaveComputedStyle( + styles: { + [cssProperty: string]: + | string + | import('@kirbydesign/designsystem/helpers').ThemeColorDefinition; + }, + pseudoElt?: string + ): boolean; } } diff --git a/libs/designsystem/testing/src/element-css-custom-matchers.ts b/libs/designsystem/testing/src/element-css-custom-matchers.ts index 54db7305c5..c65f4ab7cd 100644 --- a/libs/designsystem/testing/src/element-css-custom-matchers.ts +++ b/libs/designsystem/testing/src/element-css-custom-matchers.ts @@ -14,7 +14,8 @@ function cssPropertyMatcher(util: MatchersUtil) { return { compare: ( element: Element, - expectedStyles: { [cssProperty: string]: string | ThemeColorDefinition } + expectedStyles: { [cssProperty: string]: string | ThemeColorDefinition }, + pseudoElt?: string ) => { let allPassed = Object.keys(expectedStyles).length !== 0; const messages = []; @@ -29,7 +30,8 @@ function cssPropertyMatcher(util: MatchersUtil) { element, cssProperty, expectedStringValue, - expectedValueAlias + expectedValueAlias, + pseudoElt ); allPassed = allPassed && pass; if (message) { @@ -85,13 +87,21 @@ function compareCssProperty( element: Element, cssProperty: string, expectedValue: string, - expectedValueAlias?: string + expectedValueAlias?: string, + pseudoElt?: string ): CustomMatcherResult { - const actualValue = TestHelper.getCssProperty(element, cssProperty); + const actualValue = TestHelper.getCssProperty(element, cssProperty, pseudoElt); const pass = util.equals(actualValue, expectedValue) || !!compareSize(actualValue, expectedValue); const message = pass ? null - : getErrorMessage(element, cssProperty, actualValue, expectedValue, expectedValueAlias); + : getErrorMessage( + element, + cssProperty, + actualValue, + expectedValue, + expectedValueAlias, + pseudoElt + ); const result = { pass: pass, message: message, @@ -127,8 +137,11 @@ function getErrorMessage( cssProperty: string, actualValue: string, expectedValue: string, - expectedValueAlias?: string + expectedValueAlias?: string, + pseudoElt?: string ) { const expectedColorNameSuffix = expectedValueAlias ? ` (${expectedValueAlias})` : ''; - return `Expected [${cssProperty}] of ${element.tagName} '${actualValue}' to be '${expectedValue}'${expectedColorNameSuffix}`; + return `Expected [${cssProperty}] of ${element.tagName}${ + pseudoElt ?? '' + } '${actualValue}' to be '${expectedValue}'${expectedColorNameSuffix}`; } diff --git a/libs/designsystem/testing/src/test-helper.ts b/libs/designsystem/testing/src/test-helper.ts index cecc929a93..3ce0983d8f 100644 --- a/libs/designsystem/testing/src/test-helper.ts +++ b/libs/designsystem/testing/src/test-helper.ts @@ -78,8 +78,8 @@ export class TestHelper { }); } - public static getCssProperty(element: Element, propertyName: string) { - return window.getComputedStyle(element).getPropertyValue(propertyName).trim(); + public static getCssProperty(element: Element, propertyName: string, pseudoElt?: string) { + return window.getComputedStyle(element, pseudoElt).getPropertyValue(propertyName).trim(); } public static screensize = {